Skip to content

Commit 6b23a40

Browse files
HF-307/HF-306: teach the capability table both token dialects
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
1 parent a425d37 commit 6b23a40

4 files changed

Lines changed: 202 additions & 52 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
1717

1818
- Changed `getAvailableFunctions()` and `getFunctionDetails()` to describe only the functions the instance's license key includes, so they no longer advertise a function that would evaluate to a `#LIC!` error. A missing, invalid, or expired key does not shorten the list. [#1731](https://github.com/handsontable/hyperformula/pull/1731)
1919
- Changed the parser for the new proprietary license keys to the entitlement key format (a human-readable text ending with a machine-readable block in square brackets), following its upstream specification. This replaces the tagged key format, which was never issued to anyone. Classic 25-character license keys and `gpl-v3` are unaffected. [#1740](https://github.com/handsontable/hyperformula/pull/1740)
20+
- Changed the license capability tokens to be matched case-insensitively, and added support for the packaging group-token vocabulary (`fun:all`, `fun:<family>.<A|B|C>`, and per-function `fun:<FUNCTION_NAME>` tokens) alongside the existing package tokens (`functions_1``functions_4` and the add-ons). A key worded in either vocabulary grants the same functions. [#1741](https://github.com/handsontable/hyperformula/pull/1741)
2021

2122
## [3.4.0] - 2026-08-10
2223

src/license/CapabilityRegistry.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55

66
import {FeatureId, LicenseEntitlement} from './LicenseEntitlement'
7-
import {CAPABILITY_TABLE, CapabilityGrant} from './capabilities'
7+
import {CAPABILITY_TABLE, CapabilityGrant, normalizeCapabilityToken} from './capabilities'
88

99
/**
1010
* The capabilities a resolved {@link LicenseEntitlement} grants, ready for gate B (the
@@ -59,12 +59,13 @@ export class CapabilityRegistry {
5959
/**
6060
* Expands an entitlement's capability tokens into the concrete functions and features they
6161
* grant. An `unrestricted` entitlement short-circuits to an unrestricted result without
62-
* consulting the table at all. Expansion through `implies` is transitive and cycle-safe (a
63-
* visited set guards against a token implying itself, directly or through others); an
64-
* unrecognized token is skipped without an error.
62+
* consulting the table at all. Tokens are matched case-insensitively (the table is keyed by
63+
* the normalized spelling — see {@link normalizeCapabilityToken}). Expansion through `implies`
64+
* is transitive and cycle-safe (a visited set guards against a token implying itself, directly
65+
* or through others); an unrecognized token is skipped without an error.
6566
*
6667
* @param {LicenseEntitlement} entitlement - the entitlement to resolve, e.g. one built by
67-
* hand in a test or produced by PR 3's license-key payload adapter
68+
* hand in a test or produced by the license-key payload adapter
6869
*/
6970
public resolve(entitlement: LicenseEntitlement): ResolvedCapabilities {
7071
if (entitlement.unrestricted) {
@@ -74,10 +75,17 @@ export class CapabilityRegistry {
7475
const functions = new Set<string>()
7576
const features = new Set<FeatureId>()
7677
const visited = new Set<string>()
78+
// Walked with a read cursor rather than `queue.shift()`: `shift` is O(n) in most engines, which
79+
// made expansion O(n^2) in the number of tokens a key carries - measured at 1.4 s inside the
80+
// constructor for a key with 100 000 tokens, which the format's missing size limit allows.
81+
// `implies` still appends, so the queue has to stay a growable array.
7782
const queue = [...entitlement.capabilities]
83+
let cursor = 0
7884

79-
while (queue.length > 0) {
80-
const token = queue.shift() as string
85+
while (cursor < queue.length) {
86+
const token = normalizeCapabilityToken(queue[cursor])
87+
88+
cursor += 1
8189
if (visited.has(token)) {
8290
continue
8391
}

src/license/capabilities.ts

Lines changed: 177 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ export const FUNCTIONS_2_TOKEN = 'functions_2'
4444
export const FUNCTIONS_3_TOKEN = 'functions_3'
4545
/** Excel simulator package — the entire implemented catalog. */
4646
export const FUNCTIONS_4_TOKEN = 'functions_4'
47+
/**
48+
* The packaging doc's whole-catalog token — the excel-simulator package as that doc's own
49+
* vocabulary spells it. Grants exactly what {@link FUNCTIONS_4_TOKEN} grants.
50+
*/
51+
export const FUN_ALL_TOKEN = 'fun:all'
4752
/**
4853
* Spreadsheet Bundle add-on (2026-08-12 packages meeting). Grants {@link FeatureId.Crud},
4954
* {@link FeatureId.UndoRedo}, {@link FeatureId.Clipboard} and {@link FeatureId.Batching} — see
@@ -57,6 +62,26 @@ export const SPREADSHEET_ADDON_TOKEN = 'spreadsheet'
5762
*/
5863
export const IMPORT_EXPORT_ADDON_TOKEN = 'import_export'
5964

65+
/**
66+
* The canonical spelling of a capability token for table lookups.
67+
*
68+
* Token names are case-insensitive — the packaging doc states it outright for its `fun:*`
69+
* vocabulary, and tolerating case on the other tokens costs nothing since none of them collide
70+
* under lowercasing. Surrounding whitespace is trimmed for a sharper reason than tidiness: every
71+
* rule that reads a token has to read the SAME token, and a padded one used to be read two
72+
* different ways at once — `' feat:crud'` failed the `feat:` prefix test that decides whether a key
73+
* speaks the feature vocabulary, so the key was granted all five feature areas instead of the one
74+
* it named, while `'feat:crud '` passed that test and then missed the table, granting none.
75+
*
76+
* Normalization happens at LOOKUP, never at storage: an entitlement carries the key's own
77+
* spellings (they are diagnostics), and {@link CAPABILITY_TABLE} is keyed by the normalized form.
78+
*
79+
* @param {string} token - a capability token as the key spells it
80+
*/
81+
export function normalizeCapabilityToken(token: string): string {
82+
return token.trim().toLowerCase()
83+
}
84+
6085
/**
6186
* Describes what a capability token grants: a set of function ids, a set of {@link FeatureId}
6287
* values, and optionally other tokens it implies. `implies` is expanded recursively by
@@ -78,58 +103,120 @@ const OPERATOR_FUNCTIONS = [
78103
'HF.MULTIPLY', 'HF.NE', 'HF.POW', 'HF.UMINUS', 'HF.UNARY_PERCENT', 'HF.UPLUS',
79104
]
80105

106+
/**
107+
* The two protected built-ins. Both are named by the packaging doc (`fun:lookup.A`, `fun:info.A`)
108+
* but sit OUTSIDE the token system today — the interpreter never gate-checks a protected
109+
* function, so granting them would be dead weight that implies a restriction that does not exist.
110+
* The doc calls this a "technical limitation" on both; their tokens below are recognized but
111+
* grant nothing.
112+
*/
113+
const PROTECTED_BUILT_INS = ['OFFSET', 'VERSION']
114+
81115
// An earlier revision granted all five features from CORE_TOKEN, which made feature gating inert
82116
// by construction: no restricted key could ever lose an API area. The ratified rule:
83117
// "Feature gating should work, but the legacy keys should grant all feat:* capabilities" — legacy
84118
// keys already resolve to the unrestricted entitlement, so the carve-out costs nothing, and the
85119
// five features moved onto their own `feat:*` tokens below.
86120

87121
/**
88-
* Package membership, as the LOWEST package that includes each function.
122+
* The 21 function groups of the packaging doc, keyed by their group tokens in normalized
123+
* (lowercase) spelling — the doc writes them `fun:<family>.<A|B|C>` and declares all token names
124+
* case-insensitive.
89125
*
90-
* Transcribed from section 6 of the internal packaging design document ("HF
91-
* function groups and packages"), which supersedes the earlier evidence file this table was first
92-
* built from. The doc organizes the catalog into 21 group tokens (`fun:<family>.<A|B|C>`) and
93-
* states each package as the cumulative union of specific groups: Math engine = the `.A` groups,
94-
* Calculated fields = `.A` + `.B`, Spreadsheet = `.A` + `.B` + `.C`. Reproducing that union gives
95-
* 17 / 64 / 161 cumulative functions before the two protected built-ins below are removed;
96-
* `capability-table.spec.ts` pins the resulting counts so a later edit cannot drift from them
97-
* silently.
126+
* Transcribed 1:1 from section 6 of the internal packaging design document ("HF function groups
127+
* and packages"), INCLUDING the members that resolve to no grant here: the operators (granted by
128+
* {@link CORE_TOKEN} instead) and the protected built-ins
129+
* (outside the token system, see {@link PROTECTED_BUILT_INS}). Keeping the doc's own membership
130+
* verbatim is what makes this map the SINGLE SOURCE OF TRUTH both token dialects read from — the
131+
* package slices below are DERIVED from these groups, so moving a function between groups moves
132+
* it in both dialects at once, and `capability-table.spec.ts` pins each group's size against the
133+
* doc's published counts so a re-transcription is a reviewable diff.
98134
*
99-
* `OFFSET` and `VERSION` are named by the doc (as `fun:lookup.A` and `fun:info.A`) but are
100-
* deliberately absent from every list below: both are protected built-ins that sit OUTSIDE the
101-
* token system today — the interpreter never gate-checks a protected function, so listing them
102-
* would be dead weight that implies a restriction that does not exist. The doc calls this a
103-
* "technical limitation" on both; see `hf-306-token-vocabulary-final` for the root cause of each
104-
* (registry protection for VERSION, parse-time resolution for OFFSET) and what closing it would
105-
* take.
106-
*/
107-
const MATH_ENGINE_FUNCTIONS = [
108-
'ABS', 'AVERAGE', 'COUNT', 'IF', 'LOG', 'MAX', 'MIN', 'MOD', 'POWER', 'PRODUCT', 'ROUND', 'ROUNDDOWN',
109-
'ROUNDUP', 'SQRT', 'SUM',
135+
* The doc freezes group names as API surface: once shipped inside license keys, a rename is a
136+
* breaking change.
137+
*/
138+
export const FUNCTION_GROUPS: ReadonlyMap<string, readonly string[]> = new Map([
139+
['fun:math.a', ['ABS', 'LOG', 'MOD', 'POWER', 'PRODUCT', 'ROUND', 'ROUNDDOWN', 'ROUNDUP', 'SQRT', 'SUM']],
140+
['fun:stat.a', ['AVERAGE', 'COUNT', 'MAX', 'MIN']],
141+
['fun:logic.a', ['IF']],
142+
['fun:operator.a', [...OPERATOR_FUNCTIONS]],
143+
['fun:info.a', ['VERSION']],
144+
['fun:lookup.a', ['OFFSET']],
145+
['fun:time.b', [
146+
'DATE', 'DATEDIF', 'DATEVALUE', 'DAY', 'DAYS', 'EOMONTH', 'HOUR', 'ISOWEEKNUM', 'MINUTE', 'MONTH',
147+
'NETWORKDAYS', 'SECOND', 'TODAY', 'WEEKDAY', 'WEEKNUM', 'WORKDAY', 'YEAR',
148+
]],
149+
['fun:text.b', [
150+
'CONCATENATE', 'EXACT', 'LEFT', 'LEN', 'LOWER', 'MID', 'REPLACE', 'REPT', 'RIGHT', 'SEARCH',
151+
'SUBSTITUTE', 'TEXT', 'TRIM', 'UPPER', 'VALUE',
152+
]],
153+
['fun:logic.b', ['AND', 'FALSE', 'IFS', 'NOT', 'OR', 'SWITCH', 'TRUE', 'XOR']],
154+
['fun:math.b', ['RAND', 'RANDBETWEEN', 'SUMIF', 'SUMIFS']],
155+
['fun:stat.b', ['AVERAGEIF', 'COUNTIF', 'STDEV.S']],
156+
['fun:lookup.c', [
157+
'ADDRESS', 'CHOOSE', 'COLUMN', 'COLUMNS', 'FILTER', 'HLOOKUP', 'HSTACK', 'HYPERLINK', 'INDEX', 'MATCH',
158+
'ROW', 'ROWS', 'SORT', 'TRANSPOSE', 'UNIQUE', 'VLOOKUP', 'VSTACK', 'XLOOKUP',
159+
]],
160+
['fun:math.c', [
161+
'ACOS', 'ASIN', 'ATAN', 'ATAN2', 'CEILING', 'COS', 'EVEN', 'EXP', 'FLOOR', 'INT', 'LN', 'MROUND', 'ODD',
162+
'PI', 'QUOTIENT', 'SEQUENCE', 'SIGN', 'SIN', 'SUBTOTAL', 'SUMPRODUCT', 'SUMSQ', 'SUMXMY2', 'TAN',
163+
]],
164+
['fun:stat.c', [
165+
'AVERAGEA', 'COUNTA', 'COUNTBLANK', 'COUNTIFS', 'LARGE', 'MAXIFS', 'MEDIAN', 'MINIFS', 'PERCENTILE.INC',
166+
'SMALL', 'STDEV.P', 'STDEVA', 'STDEVPA', 'VAR.P', 'VAR.S',
167+
]],
168+
['fun:time.c', ['DAYS360', 'EDATE', 'NOW', 'TIME', 'YEARFRAC']],
169+
['fun:text.c', ['CHAR', 'CLEAN', 'CODE', 'FIND', 'PROPER', 'T', 'TEXTJOIN', 'UNICHAR']],
170+
['fun:info.c', [
171+
'ISBLANK', 'ISERR', 'ISERROR', 'ISEVEN', 'ISLOGICAL', 'ISNA', 'ISNUMBER', 'ISODD', 'ISTEXT', 'N', 'NA',
172+
]],
173+
['fun:logic.c', ['IFERROR', 'IFNA']],
174+
['fun:finance.c', ['FV', 'IPMT', 'IRR', 'NPV', 'PMT', 'PPMT', 'PV', 'RATE', 'SLN', 'XIRR', 'XNPV']],
175+
['fun:engineer.c', ['DEC2HEX', 'HEX2DEC']],
176+
['fun:array.c', ['ARRAYFORMULA', 'ARRAY_CONSTRAIN']],
177+
])
178+
179+
/** The group tokens each package adds, exactly as the packaging doc's §4 table states them. */
180+
const MATH_ENGINE_GROUPS = ['fun:math.a', 'fun:stat.a', 'fun:logic.a', 'fun:operator.a', 'fun:info.a', 'fun:lookup.a']
181+
const CALCULATED_FIELDS_GROUPS = ['fun:time.b', 'fun:text.b', 'fun:logic.b', 'fun:math.b', 'fun:stat.b']
182+
const SPREADSHEET_GROUPS = [
183+
'fun:lookup.c', 'fun:math.c', 'fun:stat.c', 'fun:time.c', 'fun:text.c', 'fun:info.c', 'fun:logic.c',
184+
'fun:finance.c', 'fun:engineer.c', 'fun:array.c',
110185
]
111186

187+
/** The members of the given groups, concatenated. The groups are disjoint, so this is a union. */
188+
function membersOfGroups(groupTokens: string[]): string[] {
189+
return groupTokens.reduce<string[]>(
190+
(members, groupToken) => members.concat(FUNCTION_GROUPS.get(groupToken) ?? []),
191+
[],
192+
)
193+
}
194+
195+
/**
196+
* The members a group contributes to a package GRANT: the group verbatim, minus the operators
197+
* (granted by {@link CORE_TOKEN} in every package) and the protected built-ins (outside the token
198+
* system entirely).
199+
*/
200+
function gatableMembersOfGroups(groupTokens: string[]): string[] {
201+
return membersOfGroups(groupTokens).filter(
202+
(name) => OPERATOR_FUNCTIONS.indexOf(name) === -1 && PROTECTED_BUILT_INS.indexOf(name) === -1,
203+
)
204+
}
205+
206+
/**
207+
* Package membership, DERIVED from {@link FUNCTION_GROUPS} as the cumulative group unions the
208+
* packaging doc's §4 table states: Math engine = the `.A` groups, Calculated fields = `.A` + `.B`,
209+
* Spreadsheet = `.A` + `.B` + `.C`. Reproducing that union gives 17 / 64 / 161 cumulative
210+
* functions before the two protected built-ins are removed; `capability-table.spec.ts` pins the
211+
* resulting full memberships by name so a re-derivation is a reviewable diff.
212+
*/
213+
const MATH_ENGINE_FUNCTIONS = gatableMembersOfGroups(MATH_ENGINE_GROUPS)
214+
112215
/** Added by the calculated-fields package, on top of {@link MATH_ENGINE_FUNCTIONS}. */
113-
const CALCULATED_FIELDS_FUNCTIONS = [
114-
'AND', 'AVERAGEIF', 'CONCATENATE', 'COUNTIF', 'DATE', 'DATEDIF', 'DATEVALUE', 'DAY', 'DAYS', 'EOMONTH',
115-
'EXACT', 'FALSE', 'HOUR', 'IFS', 'ISOWEEKNUM', 'LEFT', 'LEN', 'LOWER', 'MID', 'MINUTE', 'MONTH',
116-
'NETWORKDAYS', 'NOT', 'OR', 'RAND', 'RANDBETWEEN', 'REPLACE', 'REPT', 'RIGHT', 'SEARCH', 'SECOND',
117-
'STDEV.S', 'SUBSTITUTE', 'SUMIF', 'SUMIFS', 'SWITCH', 'TEXT', 'TODAY', 'TRIM', 'TRUE', 'UPPER', 'VALUE',
118-
'WEEKDAY', 'WEEKNUM', 'WORKDAY', 'XOR', 'YEAR',
119-
]
216+
const CALCULATED_FIELDS_FUNCTIONS = gatableMembersOfGroups(CALCULATED_FIELDS_GROUPS)
120217

121218
/** Added by the spreadsheet package, on top of {@link CALCULATED_FIELDS_FUNCTIONS}. */
122-
const SPREADSHEET_FUNCTIONS = [
123-
'ACOS', 'ADDRESS', 'ARRAYFORMULA', 'ARRAY_CONSTRAIN', 'ASIN', 'ATAN', 'ATAN2', 'AVERAGEA', 'CEILING',
124-
'CHAR', 'CHOOSE', 'CLEAN', 'CODE', 'COLUMN', 'COLUMNS', 'COS', 'COUNTA', 'COUNTBLANK', 'COUNTIFS',
125-
'DAYS360', 'DEC2HEX', 'EDATE', 'EVEN', 'EXP', 'FILTER', 'FIND', 'FLOOR', 'FV', 'HEX2DEC', 'HLOOKUP',
126-
'HSTACK', 'HYPERLINK', 'IFERROR', 'IFNA', 'INDEX', 'INT', 'IPMT', 'IRR', 'ISBLANK', 'ISERR', 'ISERROR',
127-
'ISEVEN', 'ISLOGICAL', 'ISNA', 'ISNUMBER', 'ISODD', 'ISTEXT', 'LARGE', 'LN', 'MATCH', 'MAXIFS', 'MEDIAN',
128-
'MINIFS', 'MROUND', 'N', 'NA', 'NOW', 'NPV', 'ODD', 'PERCENTILE.INC', 'PI', 'PMT', 'PPMT', 'PROPER', 'PV',
129-
'QUOTIENT', 'RATE', 'ROW', 'ROWS', 'SEQUENCE', 'SIGN', 'SIN', 'SLN', 'SMALL', 'SORT', 'STDEV.P', 'STDEVA',
130-
'STDEVPA', 'SUBTOTAL', 'SUMPRODUCT', 'SUMSQ', 'SUMXMY2', 'T', 'TAN', 'TEXTJOIN', 'TIME', 'TRANSPOSE',
131-
'UNICHAR', 'UNIQUE', 'VAR.P', 'VAR.S', 'VLOOKUP', 'VSTACK', 'XIRR', 'XLOOKUP', 'XNPV', 'YEARFRAC',
132-
]
219+
const SPREADSHEET_FUNCTIONS = gatableMembersOfGroups(SPREADSHEET_GROUPS)
133220

134221
/**
135222
* Added by the excel-simulator package, on top of {@link SPREADSHEET_FUNCTIONS} — the rest of the
@@ -187,7 +274,50 @@ const functions4Grant: CapabilityGrant = {
187274
}
188275

189276
/**
190-
* The production capability table.
277+
* One table entry per group token: the group's gatable members, so a key may assemble a package
278+
* from groups instead of naming a `functions_N` slice. `fun:info.a` and `fun:lookup.a` resolve to
279+
* EMPTY grants on purpose — their members are the protected built-ins, which are always available
280+
* and must never become table-covered (a covered function is gated for every key not granting
281+
* it). The tokens stay recognized either way, so a key carrying them is never reported as
282+
* unrecognized: they are the doc's bookkeeping identifiers for functionality every key gets.
283+
*/
284+
const groupEntries: [string, CapabilityGrant][] = Array.from(FUNCTION_GROUPS.keys()).map((groupToken) => [
285+
groupToken,
286+
{functions: gatableMembersOfGroups([groupToken]), features: []},
287+
])
288+
289+
/**
290+
* One table entry per canonical function name: the packaging doc's single-function tokens
291+
* (`fun:<CANONICAL_FUNCTION_NAME>`), "for surgical grants: custom deals, previews, per-function
292+
* exceptions". One exists for EVERY canonical name — including the operators (harmless: core
293+
* grants them anyway) and the protected built-ins (empty grants, as above). Alias names get no
294+
* token of their own: tokens reference canonical names, and an alias travels with its canonical
295+
* function because the gates canonicalize before consulting the table.
296+
*/
297+
const singleFunctionEntries: [string, CapabilityGrant][] = functions4Grant.functions
298+
.concat(OPERATOR_FUNCTIONS, PROTECTED_BUILT_INS)
299+
.map((name) => [
300+
`fun:${normalizeCapabilityToken(name)}`,
301+
{functions: PROTECTED_BUILT_INS.indexOf(name) === -1 ? [name] : [], features: []},
302+
])
303+
304+
/**
305+
* The production capability table, keyed by NORMALIZED token spelling — look up through
306+
* {@link normalizeCapabilityToken}, never with a raw key string.
307+
*
308+
* The engine understands BOTH token dialects in circulation, resolved from the one group registry
309+
* above so they cannot drift apart:
310+
*
311+
* - the key spec's package slices (`functions_1..4`) plus the two add-on tokens — the vocabulary
312+
* the upstream generator's own schema mints today;
313+
* - the packaging doc's group vocabulary (`fun:all`, `fun:<family>.<A|B|C>`,
314+
* `fun:<CANONICAL_FUNCTION_NAME>`) — §6 of the 12.08 packaging doc.
315+
*
316+
* Accepting the superset is deliberate and spec-clean: an unrecognized token is defined as "a
317+
* grant this version does not implement" (strict-shape/lenient-vocabulary, T7), so implementing
318+
* more tokens than the generator currently mints breaks nothing — and it makes the engine robust
319+
* to the still-open business decision about which dialect keys will finally be worded in
320+
* (owner's call, 20.08). A key's function set is the UNION of everything recognized.
191321
*
192322
* The grants are stored FULLY EXPANDED rather than chained through `implies`: the packaging
193323
* design states the enforcement layer must not assume a hierarchy between tokens, and that the
@@ -212,6 +342,11 @@ const functions4Grant: CapabilityGrant = {
212342
* RESERVED grant, since nothing in the public API is gated on it yet: HF-107 hasn't shipped the
213343
* feature it would gate. Both tokens stay recognized either way, so an issued key carrying one is
214344
* never reported as unrecognized.
345+
*
346+
* Entry ORDER is load-bearing at one spot: `CapabilityRegistry`'s reverse index maps each
347+
* function id to the FIRST token that lists it, so the package slices stay ahead of the group and
348+
* single-function tokens, keeping `capabilityOf`'s answers what they were before the second
349+
* dialect existed.
215350
*/
216351
export const CAPABILITY_TABLE: ReadonlyMap<string, CapabilityGrant> = new Map([
217352
[CORE_TOKEN, coreGrant],
@@ -233,5 +368,7 @@ export const CAPABILITY_TABLE: ReadonlyMap<string, CapabilityGrant> = new Map([
233368
features: [FeatureId.Crud, FeatureId.UndoRedo, FeatureId.Clipboard, FeatureId.Batching],
234369
}],
235370
[IMPORT_EXPORT_ADDON_TOKEN, {functions: [], features: [FeatureId.ImportExport]}],
371+
[FUN_ALL_TOKEN, {functions: [...functions4Grant.functions], features: []}],
372+
...groupEntries,
373+
...singleFunctionEntries,
236374
])
237-

0 commit comments

Comments
 (0)