@delmaredigital/payload-experiments 0.2.0-rc.7 → 0.3.0-rc.4

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 (336) hide show
  1. package/README.md +202 -26
  2. package/dist/admin/PreviewLinks.d.ts +2 -1
  3. package/dist/admin/PreviewLinks.js +50 -21
  4. package/dist/admin/TestsNavLink.d.ts +3 -1
  5. package/dist/admin/TestsNavLink.js +2 -2
  6. package/dist/admin/actions/ConfirmDialog.d.ts +7 -4
  7. package/dist/admin/actions/ConfirmDialog.js +37 -82
  8. package/dist/admin/actions/DecideDialog.d.ts +3 -1
  9. package/dist/admin/actions/DecideDialog.js +30 -13
  10. package/dist/admin/actions/MakeContentResult.d.ts +13 -2
  11. package/dist/admin/actions/MakeContentResult.js +115 -16
  12. package/dist/admin/actions/actions.model.d.ts +2 -4
  13. package/dist/admin/actions/actions.model.js +2 -3
  14. package/dist/admin/allocation.model.js +3 -5
  15. package/dist/admin/api.d.ts +10 -4
  16. package/dist/admin/api.js +11 -7
  17. package/dist/admin/armSelect/ArmSelectField.js +14 -22
  18. package/dist/admin/arms/arms.model.d.ts +1 -21
  19. package/dist/admin/arms/arms.model.js +1 -153
  20. package/dist/admin/copy/actions.d.ts +39 -3
  21. package/dist/admin/copy/actions.js +40 -4
  22. package/dist/admin/copy/armSelect.js +5 -5
  23. package/dist/admin/copy/arms.d.ts +1 -6
  24. package/dist/admin/copy/arms.js +1 -16
  25. package/dist/admin/copy/audience.d.ts +2 -1
  26. package/dist/admin/copy/audience.js +3 -2
  27. package/dist/admin/copy/common.d.ts +13 -6
  28. package/dist/admin/copy/common.js +14 -8
  29. package/dist/admin/copy/goals.d.ts +21 -6
  30. package/dist/admin/copy/goals.js +27 -6
  31. package/dist/admin/copy/readiness.d.ts +22 -0
  32. package/dist/admin/copy/readiness.js +21 -0
  33. package/dist/admin/copy/recommendation.d.ts +0 -3
  34. package/dist/admin/copy/recommendation.js +0 -3
  35. package/dist/admin/copy/results.d.ts +43 -2
  36. package/dist/admin/copy/results.js +44 -3
  37. package/dist/admin/copy/review.d.ts +2 -2
  38. package/dist/admin/copy/review.js +2 -2
  39. package/dist/admin/copy/setting.d.ts +2 -0
  40. package/dist/admin/copy/setting.js +2 -0
  41. package/dist/admin/copy/surface.d.ts +7 -5
  42. package/dist/admin/copy/surface.js +9 -5
  43. package/dist/admin/copy/testPage.d.ts +10 -0
  44. package/dist/admin/copy/testPage.js +10 -0
  45. package/dist/admin/format/date.d.ts +2 -0
  46. package/dist/admin/format/date.js +10 -0
  47. package/dist/admin/paths.d.ts +2 -1
  48. package/dist/admin/paths.js +2 -1
  49. package/dist/admin/previewLinks.model.d.ts +4 -2
  50. package/dist/admin/previewLinks.model.js +8 -7
  51. package/dist/admin/results/CiChart.js +11 -21
  52. package/dist/admin/results/DataQualityPanel.d.ts +4 -2
  53. package/dist/admin/results/DataQualityPanel.js +30 -17
  54. package/dist/admin/results/DataTable.d.ts +16 -0
  55. package/dist/admin/results/DataTable.js +152 -0
  56. package/dist/admin/results/StatTile.d.ts +7 -0
  57. package/dist/admin/results/StatTile.js +22 -0
  58. package/dist/admin/results/VerdictHero.d.ts +13 -0
  59. package/dist/admin/results/{VerdictCard.js → VerdictHero.js} +15 -12
  60. package/dist/admin/results/legacy.d.ts +16 -0
  61. package/dist/admin/results/legacy.js +27 -0
  62. package/dist/admin/results/releasePages.model.d.ts +17 -0
  63. package/dist/admin/results/releasePages.model.js +10 -0
  64. package/dist/admin/results/results.css +64 -0
  65. package/dist/admin/results/results.model.d.ts +11 -7
  66. package/dist/admin/results/results.model.js +24 -7
  67. package/dist/admin/results/snapshot.d.ts +14 -0
  68. package/dist/admin/results/snapshot.js +3 -0
  69. package/dist/admin/surface/ResultsRoute.d.ts +1 -1
  70. package/dist/admin/surface/ResultsRoute.js +59 -34
  71. package/dist/admin/surface/SurfaceShell.d.ts +11 -2
  72. package/dist/admin/surface/SurfaceShell.js +49 -14
  73. package/dist/admin/surface/TestsHome.d.ts +6 -1
  74. package/dist/admin/surface/TestsHome.js +182 -114
  75. package/dist/admin/surface/TestsSurface.js +56 -16
  76. package/dist/admin/surface/goals/GoalForm.js +341 -308
  77. package/dist/admin/surface/goals/GoalsList.js +123 -89
  78. package/dist/admin/surface/goals/data.d.ts +8 -2
  79. package/dist/admin/surface/goals/data.js +24 -16
  80. package/dist/admin/surface/goals/goals.css +10 -13
  81. package/dist/admin/surface/home.model.d.ts +8 -8
  82. package/dist/admin/surface/home.model.js +23 -12
  83. package/dist/admin/surface/newTest/NewTest.js +9 -9
  84. package/dist/admin/surface/pageBlock/parts.js +31 -26
  85. package/dist/admin/surface/readiness/ActionDialogs.js +47 -19
  86. package/dist/admin/surface/readiness/Banner.d.ts +3 -0
  87. package/dist/admin/surface/readiness/Banner.js +58 -33
  88. package/dist/admin/surface/readiness/SaveStatus.d.ts +1 -0
  89. package/dist/admin/surface/readiness/SaveStatus.js +6 -3
  90. package/dist/admin/surface/readiness/readiness.model.d.ts +2 -0
  91. package/dist/admin/surface/readiness/readiness.model.js +25 -9
  92. package/dist/admin/surface/readiness/useTransition.d.ts +1 -0
  93. package/dist/admin/surface/readiness/useTransition.js +2 -1
  94. package/dist/admin/surface/results/ReleasePages.d.ts +14 -0
  95. package/dist/admin/surface/results/ReleasePages.js +116 -0
  96. package/dist/admin/surface/results/ResultsBody.d.ts +11 -1
  97. package/dist/admin/surface/results/ResultsBody.js +277 -105
  98. package/dist/admin/surface/results/ResultsHeader.d.ts +10 -0
  99. package/dist/admin/surface/results/ResultsHeader.js +177 -0
  100. package/dist/admin/surface/results/recommendation.model.d.ts +3 -2
  101. package/dist/admin/surface/results/recommendation.model.js +12 -9
  102. package/dist/admin/surface/results/useResults.d.ts +21 -3
  103. package/dist/admin/surface/results/useResults.js +2 -1
  104. package/dist/admin/surface/setting/parts.js +75 -88
  105. package/dist/admin/surface/setting/setting.css +3 -15
  106. package/dist/admin/surface/surface.css +61 -47
  107. package/dist/admin/surface/test/AdvancedSection.js +55 -48
  108. package/dist/admin/surface/test/GoalSection.js +46 -47
  109. package/dist/admin/surface/test/LengthSection.js +46 -34
  110. package/dist/admin/surface/test/Rail.d.ts +4 -0
  111. package/dist/admin/surface/test/Rail.js +83 -0
  112. package/dist/admin/surface/test/ReadinessBar.d.ts +5 -0
  113. package/dist/admin/surface/test/ReadinessBar.js +33 -0
  114. package/dist/admin/surface/test/Section.d.ts +1 -1
  115. package/dist/admin/surface/test/Section.js +36 -16
  116. package/dist/admin/surface/test/TargetingSection.js +168 -142
  117. package/dist/admin/surface/test/TestPage.d.ts +2 -1
  118. package/dist/admin/surface/test/TestPage.js +146 -68
  119. package/dist/admin/surface/test/VersionsSection.js +151 -120
  120. package/dist/admin/surface/test/fields.d.ts +11 -4
  121. package/dist/admin/surface/test/fields.js +84 -85
  122. package/dist/admin/surface/test/jumpToSection.d.ts +3 -0
  123. package/dist/admin/surface/test/jumpToSection.js +10 -0
  124. package/dist/admin/surface/test/pageParts.js +9 -3
  125. package/dist/admin/surface/test/testPage.css +81 -14
  126. package/dist/admin/surface/test/testPage.model.d.ts +2 -3
  127. package/dist/admin/surface/test/testPage.model.js +1 -21
  128. package/dist/admin/surface/useTestDocument.js +12 -5
  129. package/dist/admin/targeting/targeting.model.d.ts +1 -3
  130. package/dist/admin/targeting/targeting.model.js +3 -8
  131. package/dist/admin/theme.css +115 -36
  132. package/dist/admin/theme.d.ts +38 -0
  133. package/dist/admin/theme.js +38 -1
  134. package/dist/admin/useApi.d.ts +17 -2
  135. package/dist/admin/useLayerUsage.js +14 -23
  136. package/dist/admin/views/ExperimentsRootView.js +12 -2
  137. package/dist/admin/views/TestsNavLinkServer.d.ts +2 -0
  138. package/dist/admin/views/TestsNavLinkServer.js +43 -0
  139. package/dist/arms/copy.d.ts +6 -0
  140. package/dist/arms/copy.js +16 -0
  141. package/dist/arms/model.d.ts +23 -0
  142. package/dist/arms/model.js +173 -0
  143. package/dist/collections/experiments/config.js +25 -0
  144. package/dist/collections/experiments/lockHook.js +3 -1
  145. package/dist/collections/identities.js +6 -1
  146. package/dist/collections/metrics.js +7 -0
  147. package/dist/collections/metricsGuards.d.ts +9 -0
  148. package/dist/collections/metricsGuards.js +33 -0
  149. package/dist/contentDone.d.ts +4 -0
  150. package/dist/contentDone.js +5 -0
  151. package/dist/db/appends.d.ts +6 -2
  152. package/dist/db/appends.js +10 -4
  153. package/dist/db/identities.d.ts +8 -7
  154. package/dist/db/identities.js +9 -6
  155. package/dist/db/locks.d.ts +6 -0
  156. package/dist/db/locks.js +13 -1
  157. package/dist/db/metricUsage.d.ts +19 -0
  158. package/dist/db/metricUsage.js +54 -0
  159. package/dist/db/names.d.ts +5 -0
  160. package/dist/db/names.js +5 -0
  161. package/dist/db/pageOccupancy.d.ts +12 -0
  162. package/dist/db/pageOccupancy.js +45 -0
  163. package/dist/db/publishedContent.d.ts +10 -0
  164. package/dist/db/publishedContent.js +43 -0
  165. package/dist/db/results.d.ts +2 -0
  166. package/dist/db/results.js +6 -2
  167. package/dist/db/snapshots.d.ts +18 -6
  168. package/dist/db/snapshots.js +25 -8
  169. package/dist/db/sweep.js +11 -6
  170. package/dist/endpoints/index.js +19 -1
  171. package/dist/endpoints/results.js +5 -0
  172. package/dist/endpoints/sanitizeReadinessDetail.d.ts +1 -0
  173. package/dist/endpoints/sanitizeReadinessDetail.js +2 -0
  174. package/dist/endpoints/summary.js +20 -2
  175. package/dist/endpoints/transitions.d.ts +15 -0
  176. package/dist/endpoints/transitions.js +36 -0
  177. package/dist/errors.d.ts +8 -0
  178. package/dist/errors.js +28 -0
  179. package/dist/exports/adminRsc.d.ts +1 -0
  180. package/dist/exports/adminRsc.js +1 -0
  181. package/dist/exports/index.d.ts +2 -1
  182. package/dist/exports/index.js +1 -0
  183. package/dist/exports/puck.d.ts +2 -1
  184. package/dist/exports/puck.js +1 -1
  185. package/dist/exports/puckEditor.d.ts +4 -1
  186. package/dist/exports/puckEditor.js +3 -1
  187. package/dist/exports/rsc.d.ts +6 -1
  188. package/dist/exports/rsc.js +2 -0
  189. package/dist/hooks/pageGuard.d.ts +5 -0
  190. package/dist/hooks/pageGuard.js +139 -0
  191. package/dist/lib/fetchAllDocs.d.ts +16 -0
  192. package/dist/lib/fetchAllDocs.js +56 -0
  193. package/dist/log.js +6 -3
  194. package/dist/manifest/compile.d.ts +5 -0
  195. package/dist/manifest/compile.js +5 -1
  196. package/dist/options.js +16 -0
  197. package/dist/plugin.js +30 -2
  198. package/dist/puck/ExperimentPicker.js +47 -16
  199. package/dist/puck/ExperimentSlot.d.ts +51 -1
  200. package/dist/puck/ExperimentSlot.js +23 -12
  201. package/dist/puck/copy.d.ts +6 -5
  202. package/dist/puck/copy.js +6 -5
  203. package/dist/puck/editor/CreateExperimentDrawer.js +46 -56
  204. package/dist/puck/editor/ExperimentSlotEditor.d.ts +52 -1
  205. package/dist/puck/editor/ExperimentSlotEditor.js +292 -137
  206. package/dist/puck/editor/ResultsReadout.d.ts +6 -0
  207. package/dist/puck/editor/ResultsReadout.js +99 -0
  208. package/dist/puck/editor/VersionBuilder.d.ts +34 -0
  209. package/dist/puck/editor/VersionBuilder.js +317 -0
  210. package/dist/puck/editor/armWrites.d.ts +7 -0
  211. package/dist/puck/editor/armWrites.js +71 -0
  212. package/dist/puck/editor/armsCache.d.ts +14 -0
  213. package/dist/puck/editor/armsCache.js +78 -0
  214. package/dist/puck/editor/copy.d.ts +33 -1
  215. package/dist/puck/editor/copy.js +45 -8
  216. package/dist/puck/editor/createTest.d.ts +1 -1
  217. package/dist/puck/editor/createTest.js +25 -14
  218. package/dist/puck/editor/editor.chrome.css +121 -0
  219. package/dist/puck/editor/editor.css +288 -204
  220. package/dist/puck/editor/editor.model.d.ts +4 -1
  221. package/dist/puck/editor/editor.model.js +10 -7
  222. package/dist/puck/editor/experimentOutlinePlugin.d.ts +3 -0
  223. package/dist/puck/editor/experimentOutlinePlugin.js +89 -0
  224. package/dist/puck/experimentSlotField.d.ts +2 -0
  225. package/dist/puck/experimentSlotField.js +11 -3
  226. package/dist/puck/hostContext.d.ts +10 -0
  227. package/dist/puck/hostContext.js +12 -0
  228. package/dist/puck/routes.d.ts +12 -0
  229. package/dist/puck/routes.js +11 -0
  230. package/dist/puck/slotConfig.d.ts +1 -1
  231. package/dist/puck/slotConfig.js +2 -2
  232. package/dist/results/aggregate.js +4 -3
  233. package/dist/results/analyse.d.ts +4 -1
  234. package/dist/results/analyse.js +27 -11
  235. package/dist/results/cuped.js +3 -2
  236. package/dist/results/identityCredit.d.ts +15 -7
  237. package/dist/results/identityCredit.js +32 -33
  238. package/dist/results/rolloutUnits.js +2 -1
  239. package/dist/results/snapshot.js +8 -2
  240. package/dist/results/srm.js +2 -1
  241. package/dist/sanitizeReadinessDetail.d.ts +7 -0
  242. package/dist/sanitizeReadinessDetail.js +63 -0
  243. package/dist/server/linkIdentity.d.ts +10 -1
  244. package/dist/server/linkIdentity.js +17 -15
  245. package/dist/server/metadata.d.ts +5 -0
  246. package/dist/server/metadata.js +18 -3
  247. package/dist/server/normaliseContentRelationships.d.ts +7 -0
  248. package/dist/server/normaliseContentRelationships.js +62 -0
  249. package/dist/server/recordContentChange.d.ts +1 -1
  250. package/dist/server/recordContentChange.js +17 -8
  251. package/dist/server/runResults.d.ts +3 -2
  252. package/dist/server/runResults.js +157 -35
  253. package/dist/targeting/values.d.ts +4 -0
  254. package/dist/targeting/values.js +10 -0
  255. package/dist/transitions/addMetric.js +8 -0
  256. package/dist/transitions/archive.js +2 -3
  257. package/dist/transitions/checkStart.d.ts +10 -3
  258. package/dist/transitions/checkStart.js +25 -0
  259. package/dist/transitions/contentFields.d.ts +7 -0
  260. package/dist/transitions/contentFields.js +20 -0
  261. package/dist/transitions/decide.js +22 -2
  262. package/dist/transitions/dismissContentEntry.d.ts +8 -0
  263. package/dist/transitions/dismissContentEntry.js +45 -0
  264. package/dist/transitions/makeContent.d.ts +5 -2
  265. package/dist/transitions/makeContent.js +217 -118
  266. package/dist/transitions/phaseRecord.d.ts +8 -2
  267. package/dist/transitions/placements.d.ts +3 -6
  268. package/dist/transitions/placements.js +12 -1
  269. package/dist/transitions/preflightData.d.ts +9 -1
  270. package/dist/transitions/preflightData.js +138 -20
  271. package/dist/transitions/releasePages.d.ts +8 -0
  272. package/dist/transitions/releasePages.js +26 -0
  273. package/dist/transitions/restoreContentEntry.d.ts +8 -0
  274. package/dist/transitions/restoreContentEntry.js +47 -0
  275. package/dist/transitions/start.js +62 -7
  276. package/dist/types/contracts.d.ts +24 -4
  277. package/dist/types/endpoints.d.ts +34 -3
  278. package/dist/types/index.d.ts +1 -0
  279. package/dist/types/index.js +1 -0
  280. package/dist/types/reasons.d.ts +3 -0
  281. package/dist/types/reasons.js +1 -0
  282. package/dist/types/snapshot.d.ts +6 -1
  283. package/dist/ui/Banner.d.ts +2 -0
  284. package/dist/ui/Banner.js +2 -0
  285. package/dist/ui/Breadcrumb.d.ts +7 -0
  286. package/dist/ui/Breadcrumb.js +25 -0
  287. package/dist/ui/Button.d.ts +15 -0
  288. package/dist/ui/Button.js +32 -0
  289. package/dist/ui/Card.d.ts +12 -0
  290. package/dist/ui/Card.js +40 -0
  291. package/dist/ui/Checkbox.d.ts +2 -0
  292. package/dist/ui/Checkbox.js +2 -0
  293. package/dist/{admin/components → ui}/ChipsInput.d.ts +4 -3
  294. package/dist/{admin/components → ui}/ChipsInput.js +20 -12
  295. package/dist/ui/Dialog.d.ts +13 -0
  296. package/dist/ui/Dialog.js +85 -0
  297. package/dist/ui/EmptyState.d.ts +2 -0
  298. package/dist/ui/EmptyState.js +2 -0
  299. package/dist/ui/Feedback.d.ts +24 -0
  300. package/dist/ui/Feedback.js +40 -0
  301. package/dist/ui/Inputs.d.ts +34 -0
  302. package/dist/ui/Inputs.js +225 -0
  303. package/dist/ui/NumberInput.d.ts +2 -0
  304. package/dist/ui/NumberInput.js +2 -0
  305. package/dist/ui/Range.d.ts +2 -0
  306. package/dist/ui/Range.js +2 -0
  307. package/dist/ui/SearchInput.d.ts +2 -0
  308. package/dist/ui/SearchInput.js +2 -0
  309. package/dist/ui/SectionCard.d.ts +2 -0
  310. package/dist/ui/SectionCard.js +2 -0
  311. package/dist/ui/Select.d.ts +2 -0
  312. package/dist/ui/Select.js +2 -0
  313. package/dist/ui/StatusChip.d.ts +2 -0
  314. package/dist/ui/StatusChip.js +2 -0
  315. package/dist/ui/Tabs.d.ts +16 -0
  316. package/dist/ui/Tabs.js +61 -0
  317. package/dist/ui/TextInput.d.ts +2 -0
  318. package/dist/ui/TextInput.js +2 -0
  319. package/dist/ui/Textarea.d.ts +2 -0
  320. package/dist/ui/Textarea.js +2 -0
  321. package/dist/ui/Toggle.d.ts +2 -0
  322. package/dist/ui/Toggle.js +2 -0
  323. package/dist/ui/accent.d.ts +40 -0
  324. package/dist/ui/accent.js +139 -0
  325. package/dist/ui/index.d.ts +9 -0
  326. package/dist/ui/index.js +9 -0
  327. package/dist/ui/ui.css +92 -0
  328. package/package.json +5 -2
  329. package/dist/admin/components/chipsInput.css +0 -11
  330. package/dist/admin/results/ArmTable.d.ts +0 -8
  331. package/dist/admin/results/ArmTable.js +0 -113
  332. package/dist/admin/results/VerdictCard.d.ts +0 -9
  333. package/dist/admin/surface/goals/GoalsBreadcrumb.d.ts +0 -5
  334. package/dist/admin/surface/goals/GoalsBreadcrumb.js +0 -47
  335. package/dist/admin/surface/test/TestHeader.d.ts +0 -4
  336. package/dist/admin/surface/test/TestHeader.js +0 -70
@@ -0,0 +1,89 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { createUsePuck, walkTree } from '@puckeditor/core';
4
+ import { useEffect, useMemo, useState } from 'react';
5
+ import { EXPERIMENT_SLOT_TYPE } from '../../types/puck.js';
6
+ import { useArms } from './armsCache.js';
7
+ const usePuckSelector = createUsePuck();
8
+ const slots = [
9
+ 'control',
10
+ 'variantA',
11
+ 'variantB',
12
+ 'variantC'
13
+ ];
14
+ // Puck 0.24.0 dist/chunk-DQXMVPVM.mjs:8040 (item attribute), 8126–8135
15
+ // (direct zone containers). Zones have no stable attribute; avoid the CSS-module hash.
16
+ const zone = 'div[class*="Layer-zones"]';
17
+ const chain = (count)=>Array(count).fill(zone).join(' ~ ');
18
+ function SlotRules({ item: [id, testId, emptySlots] }) {
19
+ const known = useArms(testId ?? undefined);
20
+ const [mounted, setMounted] = useState(false);
21
+ useEffect(()=>{
22
+ setMounted(true);
23
+ }, []);
24
+ // CSS.escape is a browser API: SSR and the first hydration render emit no rules.
25
+ if (!mounted) return null;
26
+ const rules = slots.flatMap((slot, index)=>{
27
+ // Original always remains available. Unknown bound tests must reveal every zone.
28
+ if (slot === 'control' || testId !== null && (!known || known.arms.includes(slot))) return [];
29
+ if (!emptySlots[index]) return [];
30
+ // Exactly the nth zone sibling: the positive chain matches n onward and the
31
+ // negated chain excludes n+1 onward. Unlike nth-child, DropLine siblings do not count.
32
+ const selector = `li[data-puck-layer-tree-id="${CSS.escape(id)}"] > ${chain(index + 1)}:not(${chain(index + 2)})`;
33
+ return [
34
+ `${selector} { display: none; }`
35
+ ];
36
+ }).join('\n');
37
+ return /*#__PURE__*/ _jsx("style", {
38
+ "data-xp-outline": id,
39
+ children: rules
40
+ });
41
+ }
42
+ function ExperimentOutline({ children }) {
43
+ // Public selector API in Puck 0.24, rather than the deprecated usePuck hook.
44
+ const key = usePuckSelector((state)=>{
45
+ const { data } = state.appState;
46
+ const items = new Map();
47
+ const collect = (content)=>{
48
+ for (const item of content)if (item.type === EXPERIMENT_SLOT_TYPE) {
49
+ const testId = item.props.experiment?.id;
50
+ items.set(item.props.id, [
51
+ item.props.id,
52
+ testId === undefined ? null : String(testId),
53
+ slots.map((slot)=>{
54
+ const content = item.props[slot];
55
+ return !Array.isArray(content) || content.length === 0;
56
+ })
57
+ ]);
58
+ }
59
+ };
60
+ walkTree(data, state.config, (content)=>{
61
+ collect(content);
62
+ });
63
+ // walkTree visits slots inside legacy zones, but does not call back on the zone itself.
64
+ for (const content of Object.values(data.zones ?? {}))collect(content);
65
+ return JSON.stringify([
66
+ ...items.values()
67
+ ]);
68
+ });
69
+ // Only binding/emptiness changes rebuild the rules, not edits to other page fields.
70
+ const rules = useMemo(()=>JSON.parse(key).map((item)=>/*#__PURE__*/ _jsx(SlotRules, {
71
+ item: item
72
+ }, item[0])), [
73
+ key
74
+ ]);
75
+ return /*#__PURE__*/ _jsxs(_Fragment, {
76
+ children: [
77
+ children,
78
+ rules
79
+ ]
80
+ });
81
+ }
82
+ /** Add to Puck's plugins so its outline wrapper composes with the host's overrides. */ export function experimentOutlinePlugin() {
83
+ return {
84
+ name: 'experiment-outline',
85
+ overrides: {
86
+ outline: ExperimentOutline
87
+ }
88
+ };
89
+ }
@@ -1,5 +1,7 @@
1
1
  import type { CustomField } from '@puckeditor/core';
2
2
  import type { ExperimentRef } from '../types/puck.js';
3
+ import { type PuckRoutes } from './routes.js';
3
4
  export declare function createExperimentSlotField(opts?: {
4
5
  label?: string;
6
+ routes?: PuckRoutes;
5
7
  }): CustomField<ExperimentRef | null>;
@@ -1,13 +1,21 @@
1
1
  import { createElement } from 'react';
2
2
  import { ExperimentPicker } from './ExperimentPicker.js';
3
+ import { resolvePuckRoutes } from './routes.js';
4
+ import { PuckHostProvider } from './hostContext.js';
3
5
  import { pickerCopy } from './copy.js';
4
6
  export function createExperimentSlotField(opts) {
7
+ const routes = resolvePuckRoutes(opts?.routes);
5
8
  return {
6
9
  type: 'custom',
7
10
  label: opts?.label ?? pickerCopy.label,
8
- render: ({ value, onChange })=>createElement(ExperimentPicker, {
9
- value,
10
- onChange
11
+ render: ({ value, onChange })=>createElement(PuckHostProvider, {
12
+ routes,
13
+ children: createElement('div', {
14
+ className: 'xp-puck-field'
15
+ }, createElement(ExperimentPicker, {
16
+ value,
17
+ onChange
18
+ }))
11
19
  })
12
20
  };
13
21
  }
@@ -0,0 +1,10 @@
1
+ import { resolvePuckRoutes } from './routes.js';
2
+ import type { ReactNode } from 'react';
3
+ export declare const usePuckRoutes: () => {
4
+ admin: string;
5
+ api: string;
6
+ };
7
+ export declare function PuckHostProvider({ routes, children }: {
8
+ routes: ReturnType<typeof resolvePuckRoutes>;
9
+ children: ReactNode;
10
+ }): import("react").JSX.Element;
@@ -0,0 +1,12 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { createContext, useContext } from 'react';
4
+ import { resolvePuckRoutes } from './routes.js';
5
+ const PuckHostContext = /*#__PURE__*/ createContext(resolvePuckRoutes());
6
+ export const usePuckRoutes = ()=>useContext(PuckHostContext);
7
+ export function PuckHostProvider({ routes, children }) {
8
+ return /*#__PURE__*/ _jsx(PuckHostContext.Provider, {
9
+ value: routes,
10
+ children: children
11
+ });
12
+ }
@@ -0,0 +1,12 @@
1
+ export interface PuckRoutes {
2
+ admin?: string;
3
+ api?: string;
4
+ }
5
+ export declare const DEFAULT_PUCK_ROUTES: {
6
+ readonly admin: "/admin";
7
+ readonly api: "/api";
8
+ };
9
+ export declare function resolvePuckRoutes(routes?: PuckRoutes): {
10
+ admin: string;
11
+ api: string;
12
+ };
@@ -0,0 +1,11 @@
1
+ // Payload's documented route defaults; hosts pass config.routes when customized.
2
+ export const DEFAULT_PUCK_ROUTES = {
3
+ admin: '/admin',
4
+ api: '/api'
5
+ };
6
+ export function resolvePuckRoutes(routes) {
7
+ return {
8
+ admin: (routes?.admin ?? DEFAULT_PUCK_ROUTES.admin).replace(/\/$/, ''),
9
+ api: (routes?.api ?? DEFAULT_PUCK_ROUTES.api).replace(/\/$/, '')
10
+ };
11
+ }
@@ -1,5 +1,5 @@
1
1
  import type { ComponentConfig } from '@puckeditor/core';
2
2
  import type { ExperimentSlotProps } from '../types/puck.js';
3
- export declare const experimentSlotLabel: "Experiment";
3
+ export declare const experimentSlotLabel = "Experiment block";
4
4
  export declare const experimentSlotFields: NonNullable<ComponentConfig<ExperimentSlotProps>['fields']>;
5
5
  export declare const experimentSlotDefaultProps: NonNullable<ComponentConfig<ExperimentSlotProps>['defaultProps']>;
@@ -1,6 +1,6 @@
1
1
  import { createExperimentSlotField } from './experimentSlotField.js';
2
- import { pickerCopy, slotLabels } from './copy.js';
3
- export const experimentSlotLabel = pickerCopy.label;
2
+ import { slotLabels } from './copy.js';
3
+ export const experimentSlotLabel = 'Experiment block';
4
4
  export const experimentSlotFields = {
5
5
  experiment: createExperimentSlotField(),
6
6
  // Puck 0.24's defaultSlots prepends each slot, reversing declaration order in the outline.
@@ -1,14 +1,15 @@
1
+ import { XpAnalysisError } from '../errors.js';
1
2
  /** Accept Date, timestamp text, or epoch milliseconds; reject invalid dates. */ export function timestampMs(ts) {
2
3
  if (!(ts instanceof Date) && typeof ts !== 'string' && typeof ts !== 'number') {
3
- throw new RangeError('Invalid timestamp');
4
+ throw new XpAnalysisError('invalid-timestamp');
4
5
  }
5
6
  const ms = ts instanceof Date ? ts.getTime() : new Date(ts).getTime();
6
- if (!Number.isFinite(ms)) throw new RangeError('Invalid timestamp');
7
+ if (!Number.isFinite(ms)) throw new XpAnalysisError('invalid-timestamp');
7
8
  return ms;
8
9
  }
9
10
  /** Attribution windows must have a finite, positive length in hours. */ export function windowMs(windowHours) {
10
11
  if (!Number.isFinite(windowHours) || windowHours <= 0) {
11
- throw new RangeError('windowHours must be a finite number greater than 0');
12
+ throw new XpAnalysisError('window-hours');
12
13
  }
13
14
  return windowHours * 3_600_000;
14
15
  }
@@ -1,4 +1,5 @@
1
1
  import type { GaussianPrior } from '@delmaredigital/experiments-stats';
2
+ import type { MetricUnavailableReason } from '../types/reasons.js';
2
3
  import type { Arm, DocId, MetricSource } from '../types/contracts.js';
3
4
  import type { Snapshot } from '../types/snapshot.js';
4
5
  import type { ExposedUnit, Timestamp } from './aggregate.js';
@@ -55,10 +56,12 @@ export interface MetricUnitData {
55
56
  values: ReadonlyMap<string, number>;
56
57
  /** Host pre-period outcomes; absent entries in a supplied map mean zero. */
57
58
  covariates?: ReadonlyMap<string, number>;
58
- unavailable?: string;
59
+ unavailable?: MetricUnavailableReason;
59
60
  }
60
61
  export type AnalysePhaseInput = {
61
62
  phase: AnalysisPhase;
63
+ /** The runner logs failures; injecting this keeps analysis free of I/O imports. */
64
+ onMetricError?: (error: unknown, metricId: DocId) => void;
62
65
  status: 'running' | 'stopped' | 'decided';
63
66
  now: Timestamp;
64
67
  /** CORE-14a firstExposures output; same-unit multi-arm units already excluded. */
@@ -1,3 +1,4 @@
1
+ import { XpAnalysisError } from '../errors.js';
1
2
  import { DEFAULT_GAUSSIAN_PRIOR, bonferroniAlpha, capValues, createThetaAdjustedStatistics, effectBayesianABTest, sequentialOneSidedTreatmentGreaterTTest, sequentialOneSidedTreatmentLesserTTest, sequentialTwoSidedTTest, statisticMean, statisticVariance } from '@delmaredigital/experiments-stats';
2
3
  import { DEFAULT_SEQUENTIAL_TUNING_PARAMETER } from '../constants.js';
3
4
  import { timestampMs } from './aggregate.js';
@@ -18,8 +19,8 @@ const severity = {
18
19
  */ export function analysePhase(input) {
19
20
  const { phase, units } = input;
20
21
  const { analysis } = phase;
21
- // Start freezes this optional collection field as-is. Match the stats engine's
22
- // default and record the effective value in every snapshot, including n = 0.
22
+ // Compatibility for phases created before Start froze the effective tuning
23
+ // value. Record that effective value in every snapshot, including n = 0.
23
24
  const sequentialTuningParameter = analysis.sequentialTuningParameter ?? DEFAULT_SEQUENTIAL_TUNING_PARAMETER;
24
25
  const alpha = analysis.alpha ?? 0.05;
25
26
  const prior = analysis.prior ?? DEFAULT_GAUSSIAN_PRIOR;
@@ -30,7 +31,7 @@ const severity = {
30
31
  const rollout = phase.phaseKind === 'rollout';
31
32
  const baselineArm = phase.arms.find((arm)=>arm.kind === (rollout ? 'holdout' : 'control'));
32
33
  const winner = rollout ? phase.arms.find((arm)=>arm.key === input.winnerArm && arm.kind !== 'holdout') : undefined;
33
- if (!baselineArm || rollout && (!winner || phase.arms.length !== 2)) throw new RangeError('Phase comparison baseline or shipped arm is missing');
34
+ if (!baselineArm || rollout && (!winner || phase.arms.length !== 2)) throw new XpAnalysisError('no-baseline');
34
35
  const compared = phase.arms.filter((arm)=>rollout ? arm.key === winner.key : arm.kind === 'variant');
35
36
  const k = compared.length;
36
37
  const alphaAdjusted = analysis.method === 'sequential' ? bonferroniAlpha(alpha, k) : alpha;
@@ -42,22 +43,22 @@ const severity = {
42
43
  }));
43
44
  const primaryDefinition = definitions.find((metric)=>metric.role === 'primary');
44
45
  if (!primaryDefinition || phase.metricDefinitions.filter((metric)=>metric.role === 'primary').length !== 1) {
45
- throw new RangeError('Phase must have exactly one primary metric');
46
+ throw new XpAnalysisError('primary-metric-count');
46
47
  }
47
48
  if (new Set(phase.metricDefinitions.map((metric)=>String(metric.metricId))).size !== phase.metricDefinitions.length || new Set(input.metricData.map((metric)=>String(metric.metricId))).size !== input.metricData.length) {
48
- throw new RangeError('Duplicate metric id');
49
+ throw new XpAnalysisError('duplicate-metric');
49
50
  }
50
51
  const from = timestampMs(phase.startedAt);
51
52
  const now = timestampMs(input.now);
52
53
  const to = Math.min(now, phase.endedAt == null ? now : timestampMs(phase.endedAt));
53
- if (to < from) throw new RangeError('Analysis window ends before the phase starts');
54
+ if (to < from) throw new XpAnalysisError('window-before-start');
54
55
  const byArm = new Map(phase.arms.map((arm)=>[
55
56
  arm.key,
56
57
  []
57
58
  ]));
58
59
  for (const [id, unit] of units){
59
60
  const ids = byArm.get(unit.arm);
60
- if (!ids) throw new RangeError('Exposed unit has an unknown arm');
61
+ if (!ids) throw new XpAnalysisError('unknown-arm');
61
62
  ids.push(id);
62
63
  }
63
64
  const perArm = Object.fromEntries([
@@ -120,17 +121,19 @@ const severity = {
120
121
  let worstArmLabel = compared[0]?.label ?? '';
121
122
  let worstArm = compared[0]?.key ?? '';
122
123
  try {
123
- if (!data || data.unavailable !== undefined) throw new Error(data?.unavailable ?? 'Metric data was not supplied');
124
+ if (!data || data.unavailable !== undefined) {
125
+ throw new XpAnalysisError('metric-not-supplied');
126
+ }
124
127
  const stats = new Map();
125
128
  const cap = kind === 'revenue' ? definition.resolvedCap ?? null : null;
126
129
  const useCovariates = analysis.cuped === 'auto' && data.covariates !== undefined && data.covariates.size > 0;
127
130
  for (const arm of phase.arms){
128
131
  const ids = byArm.get(arm.key);
129
132
  const values = ids.map((id)=>data.values.get(id) ?? 0);
130
- if (!values.every(Number.isFinite)) throw new RangeError('Metric values must be finite');
133
+ if (!values.every(Number.isFinite)) throw new XpAnalysisError('non-finite-values');
131
134
  const capped = capValues(values, cap);
132
135
  const rawPre = useCovariates ? ids.map((id)=>data.covariates.get(id) ?? 0) : undefined;
133
- if (rawPre && !rawPre.every(Number.isFinite)) throw new RangeError('Covariates must be finite');
136
+ if (rawPre && !rawPre.every(Number.isFinite)) throw new XpAnalysisError('non-finite-covariates');
134
137
  const pre = rawPre && capValues(rawPre, cap).values;
135
138
  const built = outcomeStatistic(kind, capped.values, pre);
136
139
  stats.set(arm.key, built.statistic);
@@ -257,7 +260,20 @@ const severity = {
257
260
  }
258
261
  }
259
262
  } catch (error) {
260
- metric.unavailable = error instanceof Error ? error.message : 'Metric could not be analysed';
263
+ input.onMetricError?.(error, metricId);
264
+ let reason = 'analysis-failed';
265
+ if (error instanceof XpAnalysisError) {
266
+ switch(error.code){
267
+ case 'metric-not-supplied':
268
+ reason = data?.unavailable ?? 'not-supplied';
269
+ break;
270
+ case 'non-finite-values':
271
+ case 'non-finite-covariates':
272
+ reason = error.code;
273
+ break;
274
+ }
275
+ }
276
+ metric.unavailable = reason;
261
277
  metric.perArm = [];
262
278
  metric.comparisons = [];
263
279
  metric.cupedApplied = false;
@@ -1,13 +1,14 @@
1
+ import { XpAnalysisError } from '../errors.js';
1
2
  import { buildRegressionAdjustedStatistic, buildStatistic, summarizeUnits, summarizeUnitsWithCovariate } from '@delmaredigital/experiments-stats';
2
3
  /** Build candidate CUPED statistics; the stats engine pools each pair, infers theta,
3
4
  * and falls back to the post statistic when pre-period variance/covariance is zero.
4
5
  * Missing units in a supplied pre-period map are zero-filled by the caller.
5
6
  */ export function outcomeStatistic(kind, values, covariates) {
6
7
  if (!values.every(Number.isFinite) || covariates && !covariates.every(Number.isFinite)) {
7
- throw new RangeError('Metric values and covariates must be finite');
8
+ throw new XpAnalysisError('cuped-non-finite');
8
9
  }
9
10
  if (kind === 'binomial' && (values.some((value)=>value !== 0 && value !== 1) || covariates?.some((value)=>value !== 0 && value !== 1))) {
10
- throw new RangeError('Binomial values and covariates must be 0 or 1');
11
+ throw new XpAnalysisError('cuped-binomial');
11
12
  }
12
13
  const moments = summarizeUnits(values);
13
14
  if (covariates === undefined) return {
@@ -1,16 +1,24 @@
1
1
  import type { ExposedUnit, MetricEvent, Timestamp } from './aggregate.js';
2
- /**
3
- * Spec §8.3: credit once to the earliest exposed linked visitor; conflicts are diagnostic only.
4
- * Callers must pass user ids through String() for both identities and user-keyed source events.
5
- */
6
- export declare function creditUserEvents(units: ReadonlyMap<string, ExposedUnit>, identities: readonly {
2
+ export interface IdentityLink {
7
3
  anonymousId: string;
8
4
  userId: string;
9
- }[], userEvents: readonly {
5
+ verifiedAt: Timestamp | null;
6
+ /** Earliest verification across all the user's browsers, exposed or not. */
7
+ ownedAt: Timestamp | null;
8
+ }
9
+ /** Unverified links credit only events before anyone proves account ownership.
10
+ * Never-verified accounts keep crediting without a time limit.
11
+ */
12
+ export declare function eligibleIdentityLinks<T extends IdentityLink>(links: readonly T[], ts: Timestamp): T[];
13
+ /** Credit each event to the earliest exposed eligible visitor, with a stable ID
14
+ * tie-break. A user counts once if eligible links span arms for a credited event.
15
+ * Callers must normalize user IDs with String().
16
+ */
17
+ export declare function creditUserEvents(units: ReadonlyMap<string, ExposedUnit>, identities: readonly IdentityLink[], userEvents: readonly {
10
18
  userId: string;
11
19
  value: number;
12
20
  ts: Timestamp;
13
- }[]): {
21
+ }[], onConflict?: (userId: string) => void): {
14
22
  events: MetricEvent[];
15
23
  conflicts: number;
16
24
  };
@@ -1,46 +1,45 @@
1
1
  import { timestampMs } from './aggregate.js';
2
- /**
3
- * Spec §8.3: credit once to the earliest exposed linked visitor; conflicts are diagnostic only.
4
- * Callers must pass user ids through String() for both identities and user-keyed source events.
5
- */ export function creditUserEvents(units, identities, userEvents) {
2
+ /** Unverified links credit only events before anyone proves account ownership.
3
+ * Never-verified accounts keep crediting without a time limit.
4
+ */ export function eligibleIdentityLinks(links, ts) {
5
+ const t = timestampMs(ts);
6
+ return links.filter((link)=>link.verifiedAt !== null || link.ownedAt === null || t < timestampMs(link.ownedAt));
7
+ }
8
+ /** Credit each event to the earliest exposed eligible visitor, with a stable ID
9
+ * tie-break. A user counts once if eligible links span arms for a credited event.
10
+ * Callers must normalize user IDs with String().
11
+ */ export function creditUserEvents(units, identities, userEvents, onConflict) {
12
+ const starts = new Map([
13
+ ...units
14
+ ].map(([id, unit])=>[
15
+ id,
16
+ timestampMs(unit.firstTs)
17
+ ]));
6
18
  const users = new Map();
7
- const starts = new Map();
8
- for (const [unitId, unit] of units)starts.set(unitId, timestampMs(unit.firstTs));
9
- for (const { anonymousId, userId } of identities){
10
- const unit = units.get(anonymousId);
11
- if (!unit) continue;
12
- const firstTs = starts.get(anonymousId);
13
- const user = users.get(userId);
14
- if (!user) users.set(userId, {
15
- unitId: anonymousId,
16
- firstTs,
17
- arms: new Set([
18
- unit.arm
19
- ])
20
- });
21
- else {
22
- user.arms.add(unit.arm);
23
- // Stable tie-break for simultaneous exposures, independent of identity row order.
24
- if (firstTs < user.firstTs || firstTs === user.firstTs && anonymousId < user.unitId) {
25
- user.unitId = anonymousId;
26
- user.firstTs = firstTs;
27
- }
28
- }
19
+ for (const link of identities){
20
+ if (!units.has(link.anonymousId)) continue;
21
+ const links = users.get(link.userId) ?? [];
22
+ links.push(link);
23
+ users.set(link.userId, links);
29
24
  }
30
- let conflicts = 0;
31
- for (const user of users.values())if (user.arms.size > 1) conflicts++;
25
+ for (const links of users.values())links.sort((a, b)=>starts.get(a.anonymousId) - starts.get(b.anonymousId) || (a.anonymousId < b.anonymousId ? -1 : a.anonymousId > b.anonymousId ? 1 : 0));
26
+ const conflicts = new Set();
32
27
  const events = [];
33
28
  for (const event of userEvents){
34
- timestampMs(event.ts);
35
- const user = users.get(event.userId);
36
- if (user) events.push({
37
- unitId: user.unitId,
29
+ const eligible = eligibleIdentityLinks(users.get(event.userId) ?? [], event.ts);
30
+ if (!eligible.length) continue;
31
+ events.push({
32
+ unitId: eligible[0].anonymousId,
38
33
  value: event.value,
39
34
  ts: event.ts
40
35
  });
36
+ if (new Set(eligible.map((link)=>units.get(link.anonymousId).arm)).size > 1) {
37
+ conflicts.add(event.userId);
38
+ onConflict?.(event.userId);
39
+ }
41
40
  }
42
41
  return {
43
42
  events,
44
- conflicts
43
+ conflicts: conflicts.size
45
44
  };
46
45
  }
@@ -1,7 +1,8 @@
1
+ import { XpAnalysisError } from '../errors.js';
1
2
  import { hash, layerSlice, rolloutBucketRanges } from '@delmaredigital/experiments-stats';
2
3
  import { timestampMs } from './aggregate.js';
3
4
  /** Spec §7.6/§8.1.5 (X1): first-ever rollout exposures inside assignment's exact ranges. */ export function rolloutAnalysisUnits({ exposuresAllPhases, rolloutPhase, seed, testWeights, coverage, layer }) {
4
- if (!seed.endsWith('|')) throw new RangeError('Assignment seed must end in |');
5
+ if (!seed.endsWith('|')) throw new XpAnalysisError('seed-format');
5
6
  const first = new Map();
6
7
  for (const row of exposuresAllPhases){
7
8
  const ts = timestampMs(row.ts);
@@ -1,3 +1,4 @@
1
+ import { XpAnalysisError } from '../errors.js';
1
2
  /** JSON cannot represent unbounded confidence limits. */ export function jsonInterval(ci) {
2
3
  return ci.map((bound)=>bound !== null && Number.isFinite(bound) ? bound : null);
3
4
  }
@@ -138,14 +139,19 @@ const shape = object({
138
139
  }),
139
140
  by: nullable(id)
140
141
  }))
142
+ }, {
143
+ health: list(object({
144
+ code: oneOf('version-page-unavailable', 'version-page-unchecked'),
145
+ path: string
146
+ }))
141
147
  });
142
148
  /** Validate the frozen JSON shape and headline/rollout invariants before storage.
143
149
  * This also accepts the shared CORE-13/admin fixtures without any I/O.
144
150
  */ export function validateSnapshot(value) {
145
- if (!shape(value)) throw new TypeError('Invalid Snapshot shape (all numbers must be finite)');
151
+ if (!shape(value)) throw new XpAnalysisError('snapshot-shape');
146
152
  const snapshot = value;
147
153
  if (snapshot.phaseKind === 'test' && snapshot.winnerArm !== null || snapshot.phaseKind === 'rollout' && (snapshot.winnerArm === null || !(snapshot.winnerArm in snapshot.exposures.perArm)) || snapshot.verdict.headline !== snapshot.verdict.finding + (snapshot.verdict.action ? ` ${snapshot.verdict.action}` : '') || new Date(snapshot.window.to).getTime() < new Date(snapshot.window.from).getTime()) {
148
- throw new TypeError('Invalid Snapshot invariants');
154
+ throw new XpAnalysisError('snapshot-invariants');
149
155
  }
150
156
  }
151
157
  /** Assemble the already-computed snapshot at the pure/I/O boundary. */ export function assembleSnapshot(snapshot) {
@@ -1,3 +1,4 @@
1
+ import { XpAnalysisError } from '../errors.js';
1
2
  import { checkSrm } from '@delmaredigital/experiments-stats';
2
3
  /** SRM uses exposed units and renormalised frozen weights, never allocation or
3
4
  * assignment counts. Rollout callers pass only shipped/holdout weights (100-h,h).
@@ -5,7 +6,7 @@ import { checkSrm } from '@delmaredigital/experiments-stats';
5
6
  */ export function exposureSrm(arms, perArm) {
6
7
  const active = arms.filter((arm)=>arm.weight > 0);
7
8
  const weight = active.reduce((sum, arm)=>sum + arm.weight, 0);
8
- if (weight <= 0) throw new RangeError('SRM requires positive weights');
9
+ if (weight <= 0) throw new XpAnalysisError('srm-weights');
9
10
  const expected = Object.fromEntries(arms.map((arm)=>[
10
11
  arm.key,
11
12
  arm.weight / weight
@@ -0,0 +1,7 @@
1
+ import type { ReadinessCode } from './types/endpoints.js';
2
+ /** Summary details contain identifiers only. Project known shapes rather than
3
+ * recursively copying host objects: titles, messages and placement detail are text.
4
+ * Other checks use authored client fallbacks and need no detail (including slot
5
+ * counts and layer capacity, which are not identifiers).
6
+ */
7
+ export declare function sanitizeReadinessDetail(code: ReadinessCode, detail: unknown): unknown;
@@ -0,0 +1,63 @@
1
+ const object = (value)=>value !== null && typeof value === 'object' && !Array.isArray(value);
2
+ // The runtime's slug/key alphabet, bounded even for host-provided parameter keys.
3
+ const key = (value)=>typeof value === 'string' && value.length <= 128 && /^[A-Za-z0-9_-]+$/.test(value);
4
+ const arm = (value)=>typeof value === 'string' && [
5
+ 'control',
6
+ 'variantA',
7
+ 'variantB',
8
+ 'variantC',
9
+ 'holdout'
10
+ ].includes(value);
11
+ const path = (value)=>typeof value === 'string' && value.length <= 128 && value.startsWith('/') && !value.includes('//') && !/[^A-Za-z0-9_/-]/.test(value);
12
+ const failure = (value)=>value === 'placements-failed' || value === 'forbidden' || value === 'not-found';
13
+ /** Summary details contain identifiers only. Project known shapes rather than
14
+ * recursively copying host objects: titles, messages and placement detail are text.
15
+ * Other checks use authored client fallbacks and need no detail (including slot
16
+ * counts and layer capacity, which are not identifiers).
17
+ */ export function sanitizeReadinessDetail(code, detail) {
18
+ switch(code){
19
+ case 'block-path-rule':
20
+ return object(detail) && Array.isArray(detail.rules) && detail.rules.length && detail.rules.every((index)=>Number.isSafeInteger(index) && index >= 0) ? {
21
+ rules: detail.rules
22
+ } : undefined;
23
+ case 'no-placements':
24
+ return failure(detail) ? detail : undefined;
25
+ case 'page-in-use':
26
+ {
27
+ if (!object(detail) || !Array.isArray(detail.paths)) return undefined;
28
+ const paths = detail.paths.filter(path);
29
+ return paths.length ? {
30
+ paths
31
+ } : undefined;
32
+ }
33
+ case 'page-target-unpublished':
34
+ // Paths only: no query, fragment, whitespace, URL authority or encoded text.
35
+ return path(detail) ? detail : undefined;
36
+ case 'flag-params-invalid':
37
+ {
38
+ if (!Array.isArray(detail) || !detail.length || !detail.every((item)=>object(item) && arm(item.arm) && key(item.field))) return undefined;
39
+ return detail.map((item)=>({
40
+ arm: item.arm,
41
+ field: item.field
42
+ }));
43
+ }
44
+ case 'flag-placement-missing':
45
+ {
46
+ if (failure(detail)) return detail;
47
+ if (!object(detail) || !Array.isArray(detail.missing) || !detail.missing.every(arm) || !Array.isArray(detail.placements) || !detail.placements.every((item)=>object(item) && arm(item.arm))) return undefined;
48
+ return {
49
+ missing: detail.missing,
50
+ placements: detail.placements.map((item)=>({
51
+ arm: item.arm
52
+ }))
53
+ };
54
+ }
55
+ case 'undeclared-targeting-key':
56
+ case 'invalid-targeting-rule':
57
+ return object(detail) && Array.isArray(detail.attributes) && detail.attributes.length && detail.attributes.every(key) ? {
58
+ attributes: detail.attributes
59
+ } : undefined;
60
+ default:
61
+ return undefined;
62
+ }
63
+ }
@@ -1,4 +1,12 @@
1
1
  import type { ExperimentsServerRuntime } from '../types/runtime.js';
2
+ import { type IdentityWriteOutcome } from '../db/identities.js';
3
+ export type LinkIdentityOutcome = IdentityWriteOutcome | 'failed';
4
+ /**
5
+ * verified: true means this request proves, in this browser, that the person owns
6
+ * the account: a magic-link or one-time-code callback, password sign-in, or OAuth
7
+ * callback. It does not mean "a session exists" or "the email is verified".
8
+ * Never throws into registration or sign-in.
9
+ */
2
10
  export declare function linkIdentity<TUser extends {
3
11
  id: string | number;
4
12
  } = {
@@ -6,4 +14,5 @@ export declare function linkIdentity<TUser extends {
6
14
  }>(serverRuntime: ExperimentsServerRuntime<TUser>, opts: {
7
15
  headers: Headers;
8
16
  userId: string;
9
- }): Promise<void>;
17
+ verified: boolean;
18
+ }): Promise<LinkIdentityOutcome>;