@gradial/aci 0.1.52-rc.21 → 0.1.52-rc.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/README.md +79 -5
  2. package/dist/astro/index.js +4 -0
  3. package/dist/audience-data-models.d.ts +12 -0
  4. package/dist/audience-data-models.js +3 -0
  5. package/dist/cli/mintlify-render.d.mts +1 -0
  6. package/dist/cli/mintlify-render.mjs +8 -0
  7. package/dist/collect/aci.js +1 -1
  8. package/dist/collect/aci.js.sha256 +1 -1
  9. package/dist/collect/api.d.ts +3 -0
  10. package/dist/collect/api.js +19 -0
  11. package/dist/collect/assignments.d.ts +7 -9
  12. package/dist/collect/assignments.js +23 -32
  13. package/dist/collect/batcher.d.ts +3 -2
  14. package/dist/collect/consent/envelope.d.ts +7 -1
  15. package/dist/collect/consent/envelope.js +3 -1
  16. package/dist/collect/consent/mapping.d.ts +2 -0
  17. package/dist/collect/consent/mapping.js +1 -0
  18. package/dist/collect/consenthold.d.ts +38 -0
  19. package/dist/collect/consenthold.js +82 -0
  20. package/dist/collect/declarative.js +7 -5
  21. package/dist/collect/gapbuffer.d.ts +13 -4
  22. package/dist/collect/gapbuffer.js +10 -4
  23. package/dist/collect/index.js +66 -19
  24. package/dist/collect/pageviews.d.ts +11 -11
  25. package/dist/collect/pageviews.js +27 -22
  26. package/dist/consent-banner/index.d.ts +17 -0
  27. package/dist/consent-banner/index.js +23 -0
  28. package/dist/content/provider.d.ts +8 -0
  29. package/dist/index.d.ts +1 -1
  30. package/dist/internal/domain/edge/identity.d.ts +5 -0
  31. package/dist/internal/domain/edge/identity.js +49 -13
  32. package/dist/internal/domain/edge/mimic_token.d.ts +1 -0
  33. package/dist/internal/domain/edge/mimic_token.js +74 -7
  34. package/dist/internal/domain/edge/personalization-core.d.ts +8 -33
  35. package/dist/internal/domain/edge/personalization-core.js +151 -195
  36. package/dist/internal/domain/edge/personalization_index.d.ts +17 -0
  37. package/dist/internal/domain/edge/personalization_index.js +56 -0
  38. package/dist/internal/domain/edge/record/codec.d.ts +1 -30
  39. package/dist/internal/domain/edge/record/codec.js +1 -156
  40. package/dist/internal/domain/edge/resolve.js +55 -38
  41. package/dist/internal/domain/edge/types.d.ts +41 -32
  42. package/dist/internal/domain/edge/variant_key.d.ts +3 -0
  43. package/dist/internal/domain/edge/variant_key.js +44 -0
  44. package/dist/internal/platform/collectionstore/errors.d.ts +13 -0
  45. package/dist/internal/platform/collectionstore/errors.js +28 -0
  46. package/dist/mintlify/blocks.d.ts +6 -0
  47. package/dist/mintlify/blocks.js +198 -0
  48. package/dist/mintlify/config.d.ts +3 -0
  49. package/dist/mintlify/config.js +77 -0
  50. package/dist/mintlify/index.d.ts +3 -0
  51. package/dist/mintlify/index.js +2 -0
  52. package/dist/mintlify/json.d.ts +8 -0
  53. package/dist/mintlify/json.js +37 -0
  54. package/dist/mintlify/materialize.d.ts +12 -0
  55. package/dist/mintlify/materialize.js +159 -0
  56. package/dist/mintlify/types.d.ts +69 -0
  57. package/dist/mintlify/types.js +1 -0
  58. package/dist/mintlify/validate.d.ts +7 -0
  59. package/dist/mintlify/validate.js +252 -0
  60. package/dist/next/assignments-navigation.d.ts +4 -0
  61. package/dist/next/assignments-navigation.js +18 -0
  62. package/dist/next/assignments-script.d.ts +3 -2
  63. package/dist/next/assignments-script.js +5 -5
  64. package/dist/next/error-seat.js +3 -0
  65. package/dist/next/index.d.ts +1 -0
  66. package/dist/next/index.js +3 -0
  67. package/dist/next/render.js +8 -1
  68. package/dist/react/VideoPlayer.d.ts +1 -1
  69. package/dist/react/index.d.ts +1 -1
  70. package/dist/registry.d.ts +4 -2
  71. package/dist/testing/conformance/checks.d.ts +2 -2
  72. package/dist/testing/conformance/checks.js +2 -0
  73. package/dist/testing/conformance/index.d.ts +1 -0
  74. package/dist/testing/conformance/index.js +1 -0
  75. package/dist/testing/conformance/next-stubs.d.ts +6 -0
  76. package/dist/testing/conformance/next-stubs.js +9 -0
  77. package/dist/testing/conformance/preset.d.ts +3 -2
  78. package/dist/testing/conformance/preset.js +12 -8
  79. package/dist/testing/conformance/root-identity.d.ts +31 -0
  80. package/dist/testing/conformance/root-identity.js +380 -0
  81. package/dist/testing/conformance/stubs/next-image.d.ts +7 -0
  82. package/dist/testing/conformance/stubs/next-image.js +5 -0
  83. package/dist/testing/conformance/stubs/next-link.d.ts +7 -0
  84. package/dist/testing/conformance/stubs/next-link.js +4 -0
  85. package/dist/testing/conformance/types.d.ts +2 -2
  86. package/dist/testing/index.js +2 -2
  87. package/dist/types/component.d.ts +9 -0
  88. package/package.json +11 -7
  89. package/src/cli/mintlify-render.mjs +9 -0
  90. package/src/types/component.ts +10 -0
  91. package/templates/astro/starter-content/src/components/chrome/Footer.astro +5 -3
  92. package/templates/astro/starter-content/src/components/chrome/Header.astro +5 -3
  93. package/templates/astro/starter-content/src/components/elements/Button.astro +5 -2
  94. package/templates/astro/starter-content/src/components/elements/ButtonBlock.astro +5 -3
  95. package/templates/astro/starter-content/src/components/sections/ContainerSection.astro +4 -3
  96. package/templates/astro/starter-content/src/components/sections/FeatureCard.astro +4 -3
  97. package/templates/astro/starter-content/src/components/sections/FeatureGrid.astro +4 -3
  98. package/templates/astro/starter-content/src/components/sections/HomeHero.astro +5 -3
  99. package/templates/astro/starter-content/src/components/sections/PersonalizationChoice.astro +4 -3
  100. package/templates/astro/starter-content/src/components/sections/StatsBar.astro +4 -3
  101. package/templates/astro/starter-content/src/fixtures/sections.ts +1 -1
  102. package/templates/astro/starter-content/src/stories/CompositionStoryFrame.astro +7 -2
  103. package/templates/astro/template/package.json.tmpl +4 -4
  104. package/templates/common/.agents/skills/aci/SKILL.md +50 -0
  105. package/templates/common/.agents/skills/authoring/SKILL.md +4 -3
  106. package/templates/common/.agents/skills/authoring/references/block-registry.md +15 -0
  107. package/templates/common/.agents/skills/migration/references/selector-map.md +1 -1
  108. package/templates/common/.content/schemas/site.schema.json +11 -1
  109. package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000101}/asset.json +1 -1
  110. package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000102}/asset.json +1 -1
  111. package/templates/common/starter-content/.content/pages/404/_index.json +1 -1
  112. package/templates/common/starter-content/.content/pages/{home/_index.json → _index.json} +2 -2
  113. package/templates/common/starter-content/.content/pages/about/_index.json +1 -1
  114. package/templates/common/starter-content/.content/pages/error/_index.json +1 -1
  115. package/templates/common/starter-content/.content/patterns/components/button.json +2 -2
  116. package/templates/common/starter-content/.content/patterns/components/container.json +2 -2
  117. package/templates/common/starter-content/.content/patterns/components/feature_card.json +2 -2
  118. package/templates/common/starter-content/.content/patterns/components/feature_grid.json +2 -2
  119. package/templates/common/starter-content/.content/patterns/components/home_hero.json +2 -2
  120. package/templates/common/starter-content/.content/patterns/components/site-footer.json +2 -2
  121. package/templates/common/starter-content/.content/patterns/components/site-navigation.json +2 -2
  122. package/templates/common/starter-content/.content/patterns/components/stats_bar.json +2 -2
  123. package/templates/common/starter-content/.content/patterns/layouts/default.json +2 -2
  124. package/templates/common/starter-content/.content/schemas/site.schema.json +11 -1
  125. package/templates/nextjs/starter-content/src/components/chrome/Footer.tsx +4 -3
  126. package/templates/nextjs/starter-content/src/components/chrome/Header.tsx +4 -3
  127. package/templates/nextjs/starter-content/src/components/elements/Button.tsx +4 -3
  128. package/templates/nextjs/starter-content/src/components/elements/ButtonBlock.tsx +4 -3
  129. package/templates/nextjs/starter-content/src/components/sections/ContainerSection.tsx +4 -2
  130. package/templates/nextjs/starter-content/src/components/sections/FeatureCard.tsx +4 -3
  131. package/templates/nextjs/starter-content/src/components/sections/FeatureGrid.tsx +4 -2
  132. package/templates/nextjs/starter-content/src/components/sections/HomeHero.tsx +4 -3
  133. package/templates/nextjs/starter-content/src/components/sections/PersonalizationChoice.tsx +4 -2
  134. package/templates/nextjs/starter-content/src/components/sections/StatsBar.tsx +4 -3
  135. package/templates/astro/starter-content/src/render/RenderPage.astro +0 -67
  136. package/templates/common/starter-content/.content/patterns/components/personalization_choice.json +0 -42
  137. package/templates/nextjs/starter-content/src/components/elements/AtomicBlock.tsx +0 -13
  138. /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000101}/editorial.json +0 -0
  139. /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000101}/governance.json +0 -0
  140. /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000101}/versions/v0001.json +0 -0
  141. /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000102}/editorial.json +0 -0
  142. /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000102}/governance.json +0 -0
  143. /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000102}/versions/v0001.json +0 -0
@@ -59,6 +59,20 @@ export function consentSignals(request, cookies) {
59
59
  privacyDenied,
60
60
  };
61
61
  }
62
+ // identityIssuancePermitted is the one rule for issuing a first-party identity
63
+ // on a request that carries none (ADR 0048). Privacy signals deny outright. A
64
+ // recorded consent choice — any envelope, even a malformed one — decides by
65
+ // itself, so an explicit denial holds in an opt-out country. Only a visitor
66
+ // with no recorded choice falls back to the release's opt-out countries, and
67
+ // an unknown country is never one of them. Personalization uses of the
68
+ // identity (claims, memberships, provider bootstrap) stay gated on the grant.
69
+ export function identityIssuancePermitted(consent, country, optOutCountries) {
70
+ if (consent.privacyDenied)
71
+ return false;
72
+ if (consent.hasConsent || consent.consentInvalid)
73
+ return consent.allowsIdentityTier;
74
+ return country !== '' && optOutCountries.includes(country);
75
+ }
62
76
  export function parseConsent(raw, decodeCookieValue = true) {
63
77
  let decoded = trimConsentWhitespace(raw);
64
78
  if (!decoded)
@@ -127,11 +141,17 @@ export function randomIdentity(random) {
127
141
  }
128
142
  export async function identityHashHex(namespace, value) {
129
143
  const digest = await crypto.subtle.digest('SHA-256', encoder.encode(`${namespace.trim().toLowerCase()}\x00${value.trim()}`));
130
- return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, '0')).join('');
144
+ return bytesToHex(new Uint8Array(digest));
131
145
  }
132
146
  export async function sha256Hex(value) {
133
147
  const digest = await crypto.subtle.digest('SHA-256', encoder.encode(value));
134
- return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, '0')).join('');
148
+ return bytesToHex(new Uint8Array(digest));
149
+ }
150
+ function bytesToHex(bytes) {
151
+ let value = '';
152
+ for (const byte of bytes)
153
+ value += byte.toString(16).padStart(2, '0');
154
+ return value;
135
155
  }
136
156
  // ── Claims cookie (aci_claims) ──
137
157
  export async function verifyClaims(value, keys, now) {
@@ -139,7 +159,9 @@ export async function verifyClaims(value, keys, now) {
139
159
  if (parts.length !== 4 || parts[0] !== 'v1')
140
160
  return null;
141
161
  const [, kid, body, signature] = parts;
142
- const secret = keys[kid];
162
+ // The kid is viewer-controlled: select only an owned key, never an inherited
163
+ // Object.prototype member such as `__proto__` or `constructor`.
164
+ const secret = Object.hasOwn(keys, kid) ? keys[kid] : undefined;
143
165
  if (!secret || !await verifyHMAC(`v1.${kid}.${body}`, signature, secret))
144
166
  return null;
145
167
  try {
@@ -164,26 +186,40 @@ export function expireCookie(name, secure) {
164
186
  return { name, value: '', options: { path: '/', httpOnly: true, secure, sameSite: 'lax', maxAge: 0, expires: new Date(0) } };
165
187
  }
166
188
  // ── Internal crypto helpers ──
167
- async function verifyHMAC(value, signature, secret) {
168
- try {
169
- const key = await crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['verify']);
170
- return await crypto.subtle.verify('HMAC', key, base64URLDecode(signature), encoder.encode(value));
171
- }
172
- catch {
189
+ // verifyHMAC returns a verdict about the signature, which is the only visitor
190
+ // input here: a malformed or wrong signature is `false`. A fault importing the
191
+ // key or computing the MAC — an empty or misconfigured secret, a WebCrypto
192
+ // failure — is not a verdict and propagates to the caller's documented failure
193
+ // boundary (hosting D-14's logged private 503, the collect handler's observed
194
+ // 500). Folding it into `false` would silently read every visitor as
195
+ // unidentified.
196
+ export async function verifyHMAC(value, signature, secret) {
197
+ const signatureBytes = decodeSignature(signature);
198
+ if (!signatureBytes)
173
199
  return false;
174
- }
200
+ const key = await crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['verify']);
201
+ return await crypto.subtle.verify('HMAC', key, signatureBytes, encoder.encode(value));
202
+ }
203
+ // Signatures are unpadded base64url, the only encoding the Go signers emit. A
204
+ // value outside that grammar (or of a length no base64 string can have) is
205
+ // malformed input; within it, decoding cannot fail.
206
+ const BASE64URL_SIGNATURE = /^[A-Za-z0-9_-]+$/;
207
+ function decodeSignature(signature) {
208
+ if (!BASE64URL_SIGNATURE.test(signature) || signature.length % 4 === 1)
209
+ return null;
210
+ return base64URLDecode(signature);
175
211
  }
176
- async function hmacSHA256(value, secret) {
212
+ export async function hmacSHA256(value, secret) {
177
213
  const key = await crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
178
214
  return base64URLEncode(new Uint8Array(await crypto.subtle.sign('HMAC', key, encoder.encode(value))));
179
215
  }
180
- function base64URLEncode(bytes) {
216
+ export function base64URLEncode(bytes) {
181
217
  let binary = '';
182
218
  for (const byte of bytes)
183
219
  binary += String.fromCharCode(byte);
184
220
  return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
185
221
  }
186
- function base64URLDecode(value) {
222
+ export function base64URLDecode(value) {
187
223
  const padded = value.replace(/-/g, '+').replace(/_/g, '/') + '='.repeat((4 - value.length % 4) % 4);
188
224
  const binary = atob(padded);
189
225
  const bytes = new Uint8Array(binary.length);
@@ -3,6 +3,7 @@ export interface MimicClaims {
3
3
  siteId: string;
4
4
  aud?: string[];
5
5
  seg?: string[];
6
+ arms?: Record<string, string>;
6
7
  sub?: string;
7
8
  jti?: string;
8
9
  iat: string;
@@ -31,20 +31,87 @@ export async function validateMimicToken(token, signingKey, siteId) {
31
31
  catch {
32
32
  return null;
33
33
  }
34
- if (claims.scope !== 'mimic')
34
+ // Explicit shape validation (edge-functions I-17): the Go twin rejects a
35
+ // wrong-typed present field at json.Unmarshal, so this validator must
36
+ // reject the same tokens rather than tolerating-and-defaulting them.
37
+ // Absent optional fields stay absent — addition stays compatible,
38
+ // corruption does not (design/09 claims delta).
39
+ if (typeof claims !== 'object' || claims === null || Array.isArray(claims))
35
40
  return null;
36
- if (typeof claims.siteId !== 'string' || claims.siteId !== siteId)
41
+ const raw = claims;
42
+ if (raw.scope !== 'mimic')
37
43
  return null;
38
- const aud = Array.isArray(claims.aud) ? claims.aud : [];
39
- const seg = Array.isArray(claims.seg) ? claims.seg : [];
40
- if (aud.length + seg.length === 0)
44
+ if (typeof raw.siteId !== 'string' || raw.siteId !== siteId)
45
+ return null;
46
+ const aud = shapeStringList(raw.aud);
47
+ const seg = shapeStringList(raw.seg);
48
+ const arms = shapeStringMap(raw.arms);
49
+ if (aud === null || seg === null || arms === null)
50
+ return null;
51
+ if (raw.sub !== undefined && typeof raw.sub !== 'string')
52
+ return null;
53
+ if (raw.jti !== undefined && typeof raw.jti !== 'string')
54
+ return null;
55
+ if (typeof raw.iat !== 'string' || Number.isNaN(new Date(raw.iat).getTime()))
56
+ return null;
57
+ if (typeof raw.expiresAt !== 'string')
58
+ return null;
59
+ // Amended validity rule (03, design/09): at least one entry across aud,
60
+ // seg, and arms — an arms-only token is valid; empty memberships are a
61
+ // real visitor state.
62
+ if (aud.length + seg.length + Object.keys(arms).length === 0)
41
63
  return null;
42
64
  if (aud.length > MIMIC_MAX_MEMBERSHIPS || seg.length > MIMIC_MAX_MEMBERSHIPS)
43
65
  return null;
44
- const expiresAt = new Date(claims.expiresAt);
66
+ const expiresAt = new Date(raw.expiresAt);
45
67
  if (Number.isNaN(expiresAt.getTime()) || expiresAt.getTime() <= Date.now())
46
68
  return null;
47
- return { ...claims, aud, seg };
69
+ const validated = {
70
+ scope: 'mimic',
71
+ siteId: raw.siteId,
72
+ iat: raw.iat,
73
+ expiresAt: raw.expiresAt,
74
+ };
75
+ if (aud.length > 0)
76
+ validated.aud = aud;
77
+ if (seg.length > 0)
78
+ validated.seg = seg;
79
+ if (Object.keys(arms).length > 0)
80
+ validated.arms = arms;
81
+ if (raw.sub !== undefined)
82
+ validated.sub = raw.sub;
83
+ if (raw.jti !== undefined)
84
+ validated.jti = raw.jti;
85
+ return validated;
86
+ }
87
+ // shapeStringList admits an absent field or a list of strings; anything else
88
+ // (a scalar, a list with non-string members) is the malformed shape Go's
89
+ // json.Unmarshal rejects.
90
+ function shapeStringList(value) {
91
+ if (value === undefined)
92
+ return [];
93
+ if (!Array.isArray(value))
94
+ return null;
95
+ for (const member of value) {
96
+ if (typeof member !== 'string')
97
+ return null;
98
+ }
99
+ return value;
100
+ }
101
+ // shapeStringMap is the arms-field twin of shapeStringList: an absent field
102
+ // or a string→string map; a scalar, array, or non-string value is malformed.
103
+ function shapeStringMap(value) {
104
+ if (value === undefined)
105
+ return {};
106
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
107
+ return null;
108
+ const out = {};
109
+ for (const [key, member] of Object.entries(value)) {
110
+ if (typeof member !== 'string')
111
+ return null;
112
+ out[key] = member;
113
+ }
114
+ return out;
48
115
  }
49
116
  function base64URLDecode(value) {
50
117
  const padded = value.replace(/-/g, '+').replace(/_/g, '/')
@@ -1,5 +1,11 @@
1
- import type { PersonalizationRequestContext, PersonalizationClaims, PersonalizationRoute, PersonalizationExperiment, PersonalizationIdentity, PersonalizationDecision, ClassificationData, Classification, EdgeRouteEntry } from './types.js';
2
- export declare function resolvePersonalizationDecision(request: PersonalizationRequestContext, route: PersonalizationRoute | null, claims: PersonalizationClaims | null, experiments?: Record<string, PersonalizationExperiment>, identity?: PersonalizationIdentity): PersonalizationDecision;
1
+ import type { PersonalizationRequestContext, PersonalizationClaims, PersonalizationRoute, PersonalizationExperiment, PersonalizationIdentity, PersonalizationDecision } from './types.js';
2
+ export { preparePersonalizationRoute } from './personalization_index.js';
3
+ export declare class PersonalizationResolutionError extends Error {
4
+ readonly variantKey: string;
5
+ readonly code = "personalization.tuple_missing";
6
+ constructor(variantKey: string);
7
+ }
8
+ export declare function resolvePersonalizationDecision(request: PersonalizationRequestContext, route: PersonalizationRoute | null, claims: PersonalizationClaims | null, experiments?: Record<string, PersonalizationExperiment>, identity?: PersonalizationIdentity, forcedArms?: Record<string, string>): PersonalizationDecision;
3
9
  export type ExpressionFactStatus = 'present' | 'absent' | 'unavailable';
4
10
  export interface ExpressionFact {
5
11
  status: ExpressionFactStatus;
@@ -46,37 +52,6 @@ export declare function evaluateRequestExpression(rule: {
46
52
  export declare const EXPRESSION_OPERATORS: string[];
47
53
  export declare const EXPRESSION_NUMBER_GRAMMAR: RegExp;
48
54
  export declare const EXPRESSION_TIMESTAMP_GRAMMAR: RegExp;
49
- /**
50
- * Phase 1: Classify the visitor against all release-level rules.
51
- *
52
- * @param ctx The request context (cookies, headers, geo, query)
53
- * @param data Release-level classification data (audiences, segments, experiments)
54
- * @param identity Visitor identity (for experiment bucketing). Pass undefined
55
- * if no identity or privacy denied.
56
- * @param claims The verified claims the caller already holds (eligibility
57
- * gate input, ASE-F4). Undefined/null means "no claims" —
58
- * gated experiments are skipped.
59
- * @returns Classification — matched audiences, segments, and experiment arm assignments
60
- */
61
- export declare function classify(ctx: PersonalizationRequestContext, data: ClassificationData, identity?: PersonalizationIdentity, claims?: PersonalizationClaims | null): Classification;
62
- /**
63
- * Phase 2: Decide the winning variant for a specific route.
64
- *
65
- * Uses the classification from Phase 1 for cheap set-membership lookups.
66
- * Audience/segment rules were already evaluated in Phase 1; the one rule
67
- * evaluated here is an experiment arm variant's own rule, because the arm
68
- * assignment is candidacy, not entitlement (ADR 0040).
69
- *
70
- * @param record The route entry (path, dispatch, variants)
71
- * @param classification The visitor's classification from Phase 1
72
- * @param ctx The request context (signal facts + rejection sink)
73
- * @param claims The verified claims the caller already holds — the
74
- * same object passed to classify() — so an arm variant's
75
- * audience.memberships rule resolves identically to the
76
- * resolver path and fails closed without it (ADR 0039).
77
- * @returns The decision — winning variant + experiment assignment, or nulls
78
- */
79
- export declare function decide(record: EdgeRouteEntry, classification: Classification, ctx: PersonalizationRequestContext, claims?: PersonalizationClaims | null): PersonalizationDecision;
80
55
  export declare const DEVICE_CLASSES: string[];
81
56
  /**
82
57
  * Classifies the request's device class from its own headers. Returns '' when