@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,45 @@
1
+ import { COLUMNS, TABLES } from './names.js';
2
+ import { qualifiedTable, quoteIdentifier as q } from './sql.js';
3
+ /** Page tests that occupy paths, with the arms the runtime routes on (`compileManifest`):
4
+ * - running or decided: the latest round's arms, or for a rollout the latest test round's;
5
+ * - archived with a standing result (winner, not discarded, pages not released): the latest test
6
+ * round's arms whose paths are rewritten to the winner (archivedPages), so a new test cannot
7
+ * silently take them over.
8
+ * Documents' arms may have been edited while stopped, so occupancy never reads them. */ export async function routedPageTests(exec) {
9
+ const e = COLUMNS.experiments, p = COLUMNS.phases;
10
+ const experiments = qualifiedTable(exec, TABLES.experiments), phases = qualifiedTable(exec, TABLES.phases);
11
+ const testRound = `LEFT JOIN LATERAL (SELECT ${q(p.arms)} AS arms FROM ${phases}
12
+ WHERE ${q(p.experimentId)} = x.${q(e.id)} AND ${q(p.phaseKind)} = 'test' ORDER BY ${q(p.phase)} DESC LIMIT 1) test ON true`;
13
+ const { rows } = await exec.query(`
14
+ SELECT x.${q(e.id)} AS "id", CASE WHEN latest.kind = 'rollout' THEN test.arms ELSE latest.arms END AS "arms", NULL::text AS "winnerArm"
15
+ FROM ${experiments} x
16
+ JOIN LATERAL (SELECT ${q(p.phaseKind)} AS kind, ${q(p.arms)} AS arms FROM ${phases}
17
+ WHERE ${q(p.experimentId)} = x.${q(e.id)} ORDER BY ${q(p.phase)} DESC LIMIT 1) latest ON true
18
+ ${testRound}
19
+ WHERE x.${q(e.kind)} = 'page' AND x.${q(e.status)} IN ('running', 'decided')
20
+ UNION ALL
21
+ SELECT x.${q(e.id)} AS "id", test.arms AS "arms", x.${q(e.decisionWinnerArm)} AS "winnerArm"
22
+ FROM ${experiments} x
23
+ ${testRound}
24
+ WHERE x.${q(e.kind)} = 'page' AND x.${q(e.status)} = 'archived' AND x.${q(e.decisionWinnerArm)} IS NOT NULL
25
+ AND x.${q(e.decisionDiscardedAt)} IS NULL AND x.${q(e.decisionPagesReleasedAt)} IS NULL
26
+ ORDER BY "id"`, []);
27
+ const occupied = [];
28
+ for (const row of rows){
29
+ const arms = Array.isArray(row.arms) ? row.arms : [];
30
+ if (row.winnerArm == null) {
31
+ occupied.push({
32
+ id: row.id,
33
+ arms
34
+ });
35
+ continue;
36
+ }
37
+ // compileManifest's archivedPages rule: every arm path other than the winner's.
38
+ const winnerPath = arms.find((arm)=>arm.key === row.winnerArm)?.pageTarget;
39
+ if (winnerPath) occupied.push({
40
+ id: row.id,
41
+ arms: arms.filter((arm)=>arm.pageTarget && arm.pageTarget !== winnerPath)
42
+ });
43
+ }
44
+ return occupied;
45
+ }
@@ -0,0 +1,10 @@
1
+ import type { PayloadRequest } from 'payload';
2
+ import type { DocId } from '../types/contracts.js';
3
+ /** §7.8 make-content: the published main row, so Payload's draft baseline never fills a field (KI-1). */
4
+ export declare function publishedContent(req: PayloadRequest, collection: string, id: DocId): Promise<(Record<string, unknown> & {
5
+ id: DocId;
6
+ }) | null>;
7
+ /** §7.8 make-content: the latest draft version, so restoring the draft never drops fields the manager cannot read (KI-1). */
8
+ export declare function latestDraftContent(req: PayloadRequest, collection: string, id: DocId): Promise<(Record<string, unknown> & {
9
+ id: DocId;
10
+ }) | undefined>;
@@ -0,0 +1,43 @@
1
+ // Keep this module adapter-only: the §7.8 exception does not permit Local API calls.
2
+ /** §7.8 make-content: the published main row, so Payload's draft baseline never fills a field (KI-1). */ export async function publishedContent(req, collection, id) {
3
+ return req.payload.db.findOne({
4
+ collection,
5
+ req,
6
+ where: {
7
+ id: {
8
+ equals: id
9
+ }
10
+ },
11
+ // Static allow-list marker only; adapter reads have no access layer.
12
+ overrideAccess: true
13
+ });
14
+ }
15
+ /** §7.8 make-content: the latest draft version, so restoring the draft never drops fields the manager cannot read (KI-1). */ export async function latestDraftContent(req, collection, id) {
16
+ const { docs } = await req.payload.db.findVersions({
17
+ collection,
18
+ req,
19
+ limit: 1,
20
+ pagination: false,
21
+ sort: '-updatedAt',
22
+ where: {
23
+ and: [
24
+ {
25
+ parent: {
26
+ equals: id
27
+ }
28
+ },
29
+ {
30
+ latest: {
31
+ equals: true
32
+ }
33
+ }
34
+ ]
35
+ },
36
+ // Static allow-list marker only; adapter reads have no access layer.
37
+ overrideAccess: true
38
+ });
39
+ return docs[0] ? {
40
+ ...docs[0].version,
41
+ id
42
+ } : undefined;
43
+ }
@@ -20,6 +20,8 @@ export interface MetricEventRow {
20
20
  export interface IdentityRow {
21
21
  anonymousId: string;
22
22
  userId: string;
23
+ verifiedAt: number | null;
24
+ ownedAt: number | null;
23
25
  }
24
26
  type PhaseQuery = {
25
27
  experimentId: DocId;
@@ -91,12 +91,16 @@ function phaseFilter({ experimentId, phase, endedAt }, type) {
91
91
  const i = COLUMNS.identities;
92
92
  const rows = [];
93
93
  for(let offset = 0; offset < ids.length; offset += batchSize){
94
- const result = await exec.query(`SELECT ${q(i.anonymousId)} AS "anonymousId", ${q(i.userId)} AS "userId" FROM ${qualifiedTable(exec, TABLES.identities)} WHERE ${q(i.anonymousId)} = ANY($1::text[]) ORDER BY ${q(i.anonymousId)}`, [
94
+ // Ownership is global to the user, including browsers outside these exposure batches.
95
+ const table = qualifiedTable(exec, TABLES.identities);
96
+ const result = await exec.query(`SELECT link.${q(i.anonymousId)} AS "anonymousId", link.${q(i.userId)} AS "userId", round(extract(epoch from link.${q(i.verifiedAt)}) * 1000)::bigint AS "verifiedAt", (SELECT round(extract(epoch from min(owner.${q(i.verifiedAt)})) * 1000)::bigint FROM ${table} owner WHERE owner.${q(i.userId)} = link.${q(i.userId)}) AS "ownedAt" FROM ${table} link WHERE link.${q(i.anonymousId)} = ANY($1::text[]) ORDER BY link.${q(i.anonymousId)}`, [
95
97
  ids.slice(offset, offset + batchSize)
96
98
  ]);
97
99
  rows.push(...result.rows.map((row)=>({
98
100
  anonymousId: row.anonymousId,
99
- userId: String(row.userId)
101
+ userId: String(row.userId),
102
+ verifiedAt: row.verifiedAt == null ? null : Number(row.verifiedAt),
103
+ ownedAt: row.ownedAt == null ? null : Number(row.ownedAt)
100
104
  })));
101
105
  }
102
106
  return rows;
@@ -1,9 +1,10 @@
1
1
  import type { Payload } from 'payload';
2
- import type { DocId, UnitType } from '../types/contracts.js';
2
+ import type { Arm, DocId, ExperimentKind, Status, UnitType } from '../types/contracts.js';
3
3
  import type { Snapshot } from '../types/snapshot.js';
4
4
  import type { AnalysisPhase, FrozenMetric } from '../results/analyse.js';
5
5
  import { type Executor } from './executor.js';
6
6
  export interface ResultsPhase extends AnalysisPhase {
7
+ arms: Array<AnalysisPhase['arms'][number] & Pick<Arm, 'pageTarget'>>;
7
8
  seed: string;
8
9
  allocation: number;
9
10
  metricDefinitions: Array<FrozenMetric & {
@@ -14,11 +15,18 @@ export interface ResultsPhase extends AnalysisPhase {
14
15
  }
15
16
  export interface ResultsCandidate {
16
17
  phase: ResultsPhase;
18
+ frozenSnapshot?: Snapshot | null;
19
+ kind?: ExperimentKind;
20
+ currentPhase?: number;
21
+ winnerArm?: string | null;
17
22
  unitType: UnitType;
18
23
  status: 'running' | 'stopped' | 'decided' | 'archived';
19
24
  latestComputedAt: string | null;
20
25
  testArms: Array<{
26
+ key: string;
21
27
  weight: number;
28
+ kind?: string;
29
+ pageTarget?: string;
22
30
  }>;
23
31
  layerSlug: string | null;
24
32
  layerSlices: number[] | null;
@@ -39,10 +47,14 @@ export declare function writeSnapshots(exec: Executor, snapshots: Snapshot[]): P
39
47
  /** Fetch pointers only, then the endpoint reads the bounded set through Payload
40
48
  * with req/overrideAccess:false. Called behind the readResults/manage gate. */
41
49
  export declare function newestSnapshotIds(exec: Executor, ids: DocId[], per: 'phase' | 'experiment'): Promise<DocId[]>;
42
- /** Called only behind the endpoint's readResults gate. Experiment collection
43
- * reads require manage, so read only the decision metadata needed by results. */
44
- export declare function resultsMetadata(exec: Executor, id: DocId): Promise<{
45
- status: string;
50
+ type ResultsMetadata = {
51
+ name: string | null;
52
+ kind: ExperimentKind;
53
+ status: Status;
46
54
  snapshotId: DocId | null;
47
55
  winnerArm: string | null;
48
- } | null>;
56
+ };
57
+ /** Called only behind the endpoint's readResults gate. Experiment collection
58
+ * reads require manage, so read only the identity and decision metadata needed by results. */
59
+ export declare function resultsMetadata(exec: Executor, id: DocId): Promise<ResultsMetadata | null>;
60
+ export {};
@@ -31,16 +31,33 @@ const LATE_BEACON_GRACE_MS = 24 * 60 * 60 * 1000;
31
31
  'startedAt', p.${q(p.startedAt)}, 'endedAt', p.${q(p.endedAt)}, 'seed', p.${q(p.seed)},
32
32
  'arms', p.${q(p.arms)}, 'allocation', p.${q(p.allocation)}, 'metricDefinitions', p.${q(p.metricDefinitions)},
33
33
  'analysis', p.${q(p.analysis)}, 'contentChanges', p.${q(p.contentChanges)}) AS "phase",
34
- e.${q(e.unitType)} AS "unitType", e.${q(e.status)} AS "status",
35
- (SELECT max(s.${q(s.computedAt)}) FROM ${snapshots} s WHERE s.${q(s.experimentId)} = p.${q(p.experimentId)} AND s.${q(s.phase)} = p.${q(p.phase)}) AS "latestComputedAt",
34
+ e.${q(e.kind)} AS "kind", e.${q(e.phase)} AS "currentPhase", e.${q(e.unitType)} AS "unitType", e.${q(e.status)} AS "status", e.${q(e.decisionWinnerArm)} AS "winnerArm",
35
+ latest."computedAt" AS "latestComputedAt",
36
36
  (SELECT t.${q(p.arms)} FROM ${phases} t WHERE t.${q(p.experimentId)} = p.${q(p.experimentId)} AND t.${q(p.phaseKind)} = 'test' AND t.${q(p.phase)} <= p.${q(p.phase)} ORDER BY t.${q(p.phase)} DESC LIMIT 1) AS "testArms",
37
+ frozen."snapshot" AS "frozenSnapshot",
37
38
  l.${q(l.slug)} AS "layerSlug", e.${q(e.layerSlices)} AS "layerSlices"
38
39
  FROM ${phases} p JOIN ${qualifiedTable(exec, TABLES.experiments)} e ON e.${q(e.id)} = p.${q(p.experimentId)}
39
40
  LEFT JOIN ${qualifiedTable(exec, TABLES.layers)} l ON l.${q(l.id)} = e.${q(e.layerId)}
40
- WHERE (p.${q(p.endedAt)} IS NULL OR p.${q(p.endedAt)} >= $1)
41
- AND NOT EXISTS (SELECT 1 FROM ${snapshots} frozen WHERE frozen.${q(s.experimentId)} = p.${q(p.experimentId)} AND frozen.${q(s.phase)} = p.${q(p.phase)}
42
- AND frozen.${q(s.computedAt)} > p.${q(p.endedAt)} + $2 * interval '1 millisecond')${experimentId === undefined ? '' : ` AND p.${q(p.experimentId)} = $3`}
43
- ORDER BY "latestComputedAt" NULLS FIRST, p.${q(p.experimentId)}, p.${q(p.phase)}`, params);
41
+ LEFT JOIN LATERAL (SELECT max(s.${q(s.computedAt)}) AS "computedAt" FROM ${snapshots} s
42
+ WHERE s.${q(s.experimentId)} = p.${q(p.experimentId)} AND s.${q(s.phase)} = p.${q(p.phase)}) latest ON true
43
+ LEFT JOIN LATERAL (SELECT s.${q(s.snapshot)} AS "snapshot" FROM ${snapshots} s
44
+ WHERE s.${q(s.experimentId)} = p.${q(p.experimentId)} AND s.${q(s.phase)} = p.${q(p.phase)}
45
+ AND p.${q(p.endedAt)} IS NOT NULL
46
+ AND (s.${q(s.computedAt)} > p.${q(p.endedAt)} + $2 * interval '1 millisecond' OR p.${q(p.endedAt)} < $1)
47
+ ORDER BY s.${q(s.computedAt)} DESC, s.${q(s.id)} DESC LIMIT 1) frozen ON true
48
+ WHERE ((e.${q(e.kind)} = 'page'
49
+ AND (p.${q(p.endedAt)} IS NULL OR p.${q(p.endedAt)} >= $1 OR EXISTS
50
+ (SELECT 1 FROM ${snapshots} existing WHERE existing.${q(s.experimentId)} = p.${q(p.experimentId)} AND existing.${q(s.phase)} = p.${q(p.phase)}))
51
+ AND e.${q(e.status)} IN ('running', 'decided') AND (p.${q(p.phase)} = e.${q(e.phase)}
52
+ OR p.${q(p.phase)} = (SELECT max(active.${q(p.phase)}) FROM ${phases} active WHERE active.${q(p.experimentId)} = e.${q(e.id)} AND active.${q(p.phaseKind)} = 'test')))
53
+ OR ((p.${q(p.endedAt)} IS NULL OR p.${q(p.endedAt)} >= $1)
54
+ AND NOT EXISTS (SELECT 1 FROM ${snapshots} done WHERE done.${q(s.experimentId)} = p.${q(p.experimentId)} AND done.${q(s.phase)} = p.${q(p.phase)}
55
+ AND done.${q(s.computedAt)} > p.${q(p.endedAt)} + $2 * interval '1 millisecond')))
56
+ ${experimentId === undefined ? '' : ` AND p.${q(p.experimentId)} = $3`}
57
+ ORDER BY (frozen."snapshot" IS NOT NULL),
58
+ CASE WHEN frozen."snapshot" IS NULL THEN latest."computedAt" END NULLS FIRST,
59
+ CASE WHEN frozen."snapshot" IS NOT NULL THEN random() END,
60
+ p.${q(p.experimentId)}, p.${q(p.phase)}`, params);
44
61
  return rows;
45
62
  }
46
63
  export async function writeSnapshot(exec, snapshot) {
@@ -80,9 +97,9 @@ export async function writeSnapshot(exec, snapshot) {
80
97
  return rows.map((row)=>row.id);
81
98
  }
82
99
  /** Called only behind the endpoint's readResults gate. Experiment collection
83
- * reads require manage, so read only the decision metadata needed by results. */ export async function resultsMetadata(exec, id) {
100
+ * reads require manage, so read only the identity and decision metadata needed by results. */ export async function resultsMetadata(exec, id) {
84
101
  const c = COLUMNS.experiments;
85
- const { rows } = await exec.query(`SELECT ${q(c.status)} AS "status", ${q(c.decisionSnapshotId)} AS "snapshotId", ${q(c.decisionWinnerArm)} AS "winnerArm" FROM ${qualifiedTable(exec, TABLES.experiments)} WHERE ${q(c.id)} = $1`, [
102
+ const { rows } = await exec.query(`SELECT ${q(c.name)} AS "name", ${q(c.kind)} AS "kind", ${q(c.status)} AS "status", ${q(c.decisionSnapshotId)} AS "snapshotId", ${q(c.decisionWinnerArm)} AS "winnerArm" FROM ${qualifiedTable(exec, TABLES.experiments)} WHERE ${q(c.id)} = $1`, [
86
103
  id
87
104
  ]);
88
105
  return rows[0] ?? null;
package/dist/db/sweep.js CHANGED
@@ -11,6 +11,13 @@ import { ResultsBudgetError } from './pgExecutor.js';
11
11
  const events = qualifiedTable(exec, TABLES.events), phases = qualifiedTable(exec, TABLES.phases);
12
12
  const revision = `(SELECT ${q(s.revision)} FROM ${qualifiedTable(exec, TABLES.state)} LIMIT 1)`;
13
13
  let swept = 0;
14
+ const warnNoProgress = ()=>logger?.warn('Experiment retention sweep made no progress before its budget expired; eligible rows may remain.');
15
+ // Every budget-limited exit with nothing swept is reported, whether the budget ran out mid-statement (the catch
16
+ // below) or before the next batch could start (here).
17
+ const finish = ()=>{
18
+ if (swept === 0 && !shouldContinue()) warnNoProgress();
19
+ return swept;
20
+ };
14
21
  // Each statement still uses the caller's budget/race-aware executor.
15
22
  async function batches(predicate, order, params = []) {
16
23
  while(shouldContinue()){
@@ -38,7 +45,7 @@ import { ResultsBudgetError } from './pgExecutor.js';
38
45
  type
39
46
  ]);
40
47
  }
41
- if (!shouldContinue()) return swept;
48
+ if (!shouldContinue()) return finish();
42
49
  // Final-endedAt retention protects every phase while any phase is open.
43
50
  const ended = await exec.query(`SELECT ${q(p.experimentId)} AS "experimentId" FROM ${phases}
44
51
  WHERE ${q(p.experimentId)} IS NOT NULL GROUP BY ${q(p.experimentId)}
@@ -54,7 +61,7 @@ import { ResultsBudgetError } from './pgExecutor.js';
54
61
  cutoff
55
62
  ]);
56
63
  }
57
- if (!shouldContinue()) return swept;
64
+ if (!shouldContinue()) return finish();
58
65
  // Every protected window ends at/after cutoff (or is open). For old
59
66
  // conversions their union therefore starts at the earliest lookback start;
60
67
  // everything before that single bound is safe. Compute it once, in the same
@@ -80,11 +87,9 @@ import { ResultsBudgetError } from './pgExecutor.js';
80
87
  before,
81
88
  frozen.revision
82
89
  ]);
83
- return swept;
90
+ return finish();
84
91
  } catch (error) {
85
- if (swept === 0 && error instanceof ResultsBudgetError) {
86
- logger?.warn('Experiment retention sweep made no progress before its budget expired; eligible rows may remain.');
87
- }
92
+ if (swept === 0 && error instanceof ResultsBudgetError) warnNoProgress();
88
93
  throw error;
89
94
  }
90
95
  }
@@ -4,7 +4,7 @@ import { XpNotImplemented, XpRefusal } from '../errors.js';
4
4
  import { readUnitCookie } from '../runtime/cookie.js';
5
5
  import { handleManifest } from './manifest.js';
6
6
  import { handleEvents } from './events.js';
7
- import { handleStart, handleAllocate, handleAddMetric, handleStop, handleDecide, handleReleaseHoldout, handleMakeContent, handleArchive } from './transitions.js';
7
+ import { handleStart, handleAllocate, handleAddMetric, handleStop, handleDecide, handleReleaseHoldout, handleReleasePages, handleMakeContent, handleDismissContentEntry, handleRestoreContentEntry, handleArchive } from './transitions.js';
8
8
  import { handlePreflight } from './preflight.js';
9
9
  import { handlePreviewToken } from './previewToken.js';
10
10
  import { handleResults } from './results.js';
@@ -59,12 +59,30 @@ export const ROUTES = [
59
59
  access: 'manage',
60
60
  handler: handleReleaseHoldout
61
61
  },
62
+ {
63
+ method: 'post',
64
+ path: '/xp/experiments/:id/release-pages',
65
+ access: 'manage',
66
+ handler: handleReleasePages
67
+ },
62
68
  {
63
69
  method: 'post',
64
70
  path: '/xp/experiments/:id/make-content',
65
71
  access: 'manage',
66
72
  handler: handleMakeContent
67
73
  },
74
+ {
75
+ method: 'post',
76
+ path: '/xp/experiments/:id/content-report/dismiss',
77
+ access: 'manage',
78
+ handler: handleDismissContentEntry
79
+ },
80
+ {
81
+ method: 'post',
82
+ path: '/xp/experiments/:id/content-report/restore',
83
+ access: 'manage',
84
+ handler: handleRestoreContentEntry
85
+ },
68
86
  {
69
87
  method: 'post',
70
88
  path: '/xp/experiments/:id/archive',
@@ -111,6 +111,11 @@ export async function handleResults(req, _ctx) {
111
111
  if (selected !== null && !records.some((record)=>record.phase === phase)) return resultsError(404, 'not-found', 'Round not found');
112
112
  const snapshot = snapshots.find((snapshot)=>snapshot.phase === phase)?.snapshot ?? null;
113
113
  const response = {
114
+ experiment: {
115
+ name: metadata.name ?? '',
116
+ kind: metadata.kind,
117
+ status: metadata.status
118
+ },
114
119
  phases,
115
120
  snapshot,
116
121
  ...decision ? {
@@ -0,0 +1 @@
1
+ export { sanitizeReadinessDetail } from '../sanitizeReadinessDetail.js';
@@ -0,0 +1,2 @@
1
+ // Shared with the admin home; keep this server import path stable.
2
+ export { sanitizeReadinessDetail } from '../sanitizeReadinessDetail.js';
@@ -2,6 +2,7 @@ import { checkStart } from '../transitions/checkStart.js';
2
2
  import { loadStartInputsBatch } from '../transitions/preflightData.js';
3
3
  import { readSnapshots, resultsError } from './results.js';
4
4
  import { experimentId } from './routeId.js';
5
+ import { sanitizeReadinessDetail } from './sanitizeReadinessDetail.js';
5
6
  export async function handleSummary(req, { options }) {
6
7
  let ids;
7
8
  try {
@@ -40,7 +41,13 @@ export async function handleSummary(req, { options }) {
40
41
  verdict: {
41
42
  code: snapshot.verdict.code,
42
43
  headline: snapshot.verdict.headline
43
- }
44
+ },
45
+ ...snapshot.health ? {
46
+ health: snapshot.health.slice(0, 100).map(({ code, path })=>({
47
+ code,
48
+ path: path.slice(0, 128)
49
+ }))
50
+ } : {}
44
51
  } : {
45
52
  exposures: 0,
46
53
  days: 0,
@@ -72,7 +79,18 @@ export async function handleSummary(req, { options }) {
72
79
  if (!experiment) continue;
73
80
  const input = inputs.get(String(experiment.id));
74
81
  try {
75
- response[key].ready = !!input && checkStart(input).every((check)=>check.ok);
82
+ const checks = input ? checkStart(input) : [];
83
+ response[key].ready = !!input && checks.every((check)=>check.ok);
84
+ const next = checks.find((check)=>!check.ok && !check.warning);
85
+ if (next) {
86
+ const detail = sanitizeReadinessDetail(next.code, next.detail);
87
+ response[key].next = {
88
+ code: next.code,
89
+ ...detail === undefined ? {} : {
90
+ detail
91
+ }
92
+ };
93
+ }
76
94
  } catch {
77
95
  response[key].ready = false;
78
96
  }
@@ -35,6 +35,21 @@ export declare function handleMakeContent<TUser extends {
35
35
  } = {
36
36
  id: string | number;
37
37
  }>(req: PayloadRequest, ctx: EndpointContext<TUser>): Promise<Response>;
38
+ export declare function handleReleasePages<TUser extends {
39
+ id: string | number;
40
+ } = {
41
+ id: string | number;
42
+ }>(req: PayloadRequest, _ctx: EndpointContext<TUser>): Promise<Response>;
43
+ export declare function handleDismissContentEntry<TUser extends {
44
+ id: string | number;
45
+ } = {
46
+ id: string | number;
47
+ }>(req: PayloadRequest, _ctx: EndpointContext<TUser>): Promise<Response>;
48
+ export declare function handleRestoreContentEntry<TUser extends {
49
+ id: string | number;
50
+ } = {
51
+ id: string | number;
52
+ }>(req: PayloadRequest, _ctx: EndpointContext<TUser>): Promise<Response>;
38
53
  export declare function handleArchive<TUser extends {
39
54
  id: string | number;
40
55
  } = {
@@ -5,6 +5,9 @@ import { addMetric } from '../transitions/addMetric.js';
5
5
  import { stop } from '../transitions/stop.js';
6
6
  import { decide } from '../transitions/decide.js';
7
7
  import { releaseHoldout } from '../transitions/releaseHoldout.js';
8
+ import { releasePages } from '../transitions/releasePages.js';
9
+ import { dismissContentEntry } from '../transitions/dismissContentEntry.js';
10
+ import { restoreContentEntry } from '../transitions/restoreContentEntry.js';
8
11
  import { makeContent } from '../transitions/makeContent.js';
9
12
  import { archive } from '../transitions/archive.js';
10
13
  import { routeId } from './routeId.js';
@@ -135,6 +138,39 @@ export async function handleMakeContent(req, ctx) {
135
138
  options: ctx.options
136
139
  }));
137
140
  }
141
+ export async function handleReleasePages(req, _ctx) {
142
+ const value = await body(req);
143
+ if (!value || !only(value, [])) return invalidBody();
144
+ return respond(req, (id)=>releasePages(req, {
145
+ experimentId: id
146
+ }));
147
+ }
148
+ export async function handleDismissContentEntry(req, _ctx) {
149
+ const value = await body(req);
150
+ if (!value || !only(value, [
151
+ 'collection',
152
+ 'id'
153
+ ]) || typeof value.collection !== 'string' || !value.collection.trim() || !(typeof value.id === 'string' && value.id.trim() || typeof value.id === 'number' && Number.isSafeInteger(value.id))) return invalidBody();
154
+ const collection = value.collection, id = value.id;
155
+ return respond(req, (experimentId)=>dismissContentEntry(req, {
156
+ experimentId,
157
+ collection,
158
+ id
159
+ }));
160
+ }
161
+ export async function handleRestoreContentEntry(req, _ctx) {
162
+ const value = await body(req);
163
+ if (!value || !only(value, [
164
+ 'collection',
165
+ 'id'
166
+ ]) || typeof value.collection !== 'string' || !value.collection.trim() || !(typeof value.id === 'string' && value.id.trim() || typeof value.id === 'number' && Number.isSafeInteger(value.id))) return invalidBody();
167
+ const collection = value.collection, id = value.id;
168
+ return respond(req, (experimentId)=>restoreContentEntry(req, {
169
+ experimentId,
170
+ collection,
171
+ id
172
+ }));
173
+ }
138
174
  export async function handleArchive(req, _ctx) {
139
175
  const value = await body(req);
140
176
  if (!value || !only(value, [
package/dist/errors.d.ts CHANGED
@@ -25,3 +25,11 @@ export declare class XpRefusal extends Error {
25
25
  readonly isPublic = true;
26
26
  constructor(code: RefusalCode, message: string, detail: unknown | undefined, status: 409 | 422 | 500 | 404);
27
27
  }
28
+ /** Fixed analysis diagnostics: callers can choose a code, never supply log text. */
29
+ export type AnalysisErrorCode = 'no-baseline' | 'primary-metric-count' | 'duplicate-metric' | 'invalid-timestamp' | 'window-hours' | 'window-before-start' | 'unknown-arm' | 'metric-not-supplied' | 'non-finite-values' | 'non-finite-covariates' | 'source-unavailable' | 'unit-type-unavailable' | 'srm-weights' | 'cuped-non-finite' | 'cuped-binomial' | 'seed-format' | 'snapshot-shape' | 'snapshot-invariants';
30
+ export declare const ANALYSIS_ERROR_MESSAGES: Readonly<Record<AnalysisErrorCode, string>>;
31
+ export declare class XpAnalysisError extends Error {
32
+ name: string;
33
+ readonly code: AnalysisErrorCode;
34
+ constructor(code: AnalysisErrorCode);
35
+ }
package/dist/errors.js CHANGED
@@ -31,3 +31,31 @@ export class XpRefusal extends Error {
31
31
  super(message), this.code = code, this.detail = detail, this.status = status;
32
32
  }
33
33
  }
34
+ export const ANALYSIS_ERROR_MESSAGES = Object.freeze({
35
+ 'no-baseline': 'Phase comparison baseline or shipped arm is missing',
36
+ 'primary-metric-count': 'Phase must have exactly one primary metric',
37
+ 'duplicate-metric': 'Duplicate metric id',
38
+ 'invalid-timestamp': 'Event or exposure timestamp is invalid',
39
+ 'window-hours': 'Attribution window must be a finite number of hours greater than 0',
40
+ 'window-before-start': 'Analysis window ends before the phase starts',
41
+ 'unknown-arm': 'Exposed unit has an unknown arm',
42
+ 'metric-not-supplied': 'Metric data was not supplied',
43
+ 'non-finite-values': 'Metric values must be finite',
44
+ 'non-finite-covariates': 'Covariates must be finite',
45
+ 'source-unavailable': 'Metric source unavailable',
46
+ 'unit-type-unavailable': 'Metric unit type unavailable',
47
+ 'srm-weights': 'SRM requires positive weights',
48
+ 'cuped-non-finite': 'Metric values and covariates must be finite',
49
+ 'cuped-binomial': 'Binomial values and covariates must be 0 or 1',
50
+ 'seed-format': 'Assignment seed must end in |',
51
+ 'snapshot-shape': 'Invalid Snapshot shape (all numbers must be finite)',
52
+ 'snapshot-invariants': 'Invalid Snapshot invariants'
53
+ });
54
+ export class XpAnalysisError extends Error {
55
+ name = 'XpAnalysisError';
56
+ code;
57
+ constructor(code){
58
+ super(ANALYSIS_ERROR_MESSAGES[code]);
59
+ this.code = code;
60
+ }
61
+ }
@@ -1,3 +1,4 @@
1
1
  export { ExperimentsRootView } from '../admin/views/ExperimentsRootView.js';
2
2
  export { CollectionListRedirect } from '../admin/views/CollectionListRedirect.js';
3
3
  export { CollectionEditRedirect } from '../admin/views/CollectionEditRedirect.js';
4
+ export { TestsNavLinkServer } from '../admin/views/TestsNavLinkServer.js';
@@ -1,3 +1,4 @@
1
1
  export { ExperimentsRootView } from '../admin/views/ExperimentsRootView.js';
2
2
  export { CollectionListRedirect } from '../admin/views/CollectionListRedirect.js';
3
3
  export { CollectionEditRedirect } from '../admin/views/CollectionEditRedirect.js';
4
+ export { TestsNavLinkServer } from '../admin/views/TestsNavLinkServer.js';
@@ -1,4 +1,5 @@
1
1
  export { createExperimentsPlugin } from '../plugin.js';
2
2
  export { defineFlagPoint, defineMetricSource } from '../define.js';
3
+ export { contentDone } from '../contentDone.js';
3
4
  export { createArmSelectField } from '../fields/armSelect.js';
4
- export type { SerializedFlagPoint, SerializedMetricSource, SerializedAttributeKeys, EditableField, Assignment, Assignments, FlagPointResult, ClientAssignments, RenderedExposure, DocId, Attributes, ExposurePolicy, ExperimentKind, Status, UnitType, ArmKey, ArmKind, TargetingRule, Arm, ParamField, MetricSource, FlagPoint, RateLimiter, ExperimentsAccess, ExperimentsPluginOptions, ArmIssueCode, RefusalCode, ReadinessCode, ErrorEnvelope, TransitionSuccess, AllocateBody, AddMetricBody, DecideBody, ArchiveBody, MakeContentResult, PreflightResult, PreviewTokenResult, PhaseSummary, ResultsResponse, SummaryResponse, EventBody, FlagDefault, Manifest, ManifestExperiment, ArmSlot, ExperimentRef, ExperimentSlotProps, ExperimentsProxyRuntimeOptions, ExperimentsProxyRuntime, ExperimentsServerRuntimeOptions, ExperimentsServerRuntime, VerdictCode, Snapshot, } from '../types/index.js';
5
+ export type { SerializedFlagPoint, SerializedMetricSource, SerializedAttributeKeys, EditableField, Assignment, Assignments, FlagPointResult, ClientAssignments, RenderedExposure, DocId, PreviewPlacement, Attributes, ExposurePolicy, ExperimentKind, Status, UnitType, ArmKey, ArmKind, TargetingRule, Arm, ParamField, MetricSource, FlagPoint, RateLimiter, ExperimentsAccess, ExperimentsPluginOptions, ExperimentsPageCollection, ArmIssueCode, RefusalCode, ReadinessCode, ErrorEnvelope, TransitionSuccess, AllocateBody, AddMetricBody, DecideBody, ArchiveBody, MakeContentResult, DismissContentEntryBody, DismissContentEntryResult, RestoreContentEntryBody, RestoreContentEntryResult, ContentReport, PreflightResult, PreviewTokenResult, PhaseSummary, ResultsResponse, SummaryResponse, EventBody, FlagDefault, Manifest, ManifestExperiment, ArmSlot, ExperimentRef, ExperimentSlotProps, ExperimentsProxyRuntimeOptions, ExperimentsProxyRuntime, ExperimentsServerRuntimeOptions, ExperimentsServerRuntime, VerdictCode, Snapshot, MetricUnavailableReason, ContentReportReason, } from '../types/index.js';
@@ -1,3 +1,4 @@
1
1
  export { createExperimentsPlugin } from '../plugin.js';
2
2
  export { defineFlagPoint, defineMetricSource } from '../define.js';
3
+ export { contentDone } from '../contentDone.js';
3
4
  export { createArmSelectField } from '../fields/armSelect.js';
@@ -1,4 +1,5 @@
1
- export { ExperimentSlot } from '../puck/ExperimentSlot.js';
1
+ export { ExperimentSlot, createExperimentSlot } from '../puck/ExperimentSlot.js';
2
2
  export { createExperimentSlotField } from '../puck/experimentSlotField.js';
3
3
  export { EXPERIMENT_SLOT_TYPE } from '../types/puck.js';
4
4
  export type { ArmSlot, ExperimentRef, ExperimentSlotProps } from '../types/puck.js';
5
+ export type { PuckRoutes } from '../puck/routes.js';
@@ -1,3 +1,3 @@
1
- export { ExperimentSlot } from '../puck/ExperimentSlot.js';
1
+ export { ExperimentSlot, createExperimentSlot } from '../puck/ExperimentSlot.js';
2
2
  export { createExperimentSlotField } from '../puck/experimentSlotField.js';
3
3
  export { EXPERIMENT_SLOT_TYPE } from '../types/puck.js';
@@ -1 +1,4 @@
1
- export { ExperimentSlotEditor } from '../puck/editor/ExperimentSlotEditor.js';
1
+ export { ExperimentSlotEditor, createExperimentSlotEditor } from '../puck/editor/ExperimentSlotEditor.js';
2
+ export { experimentOutlinePlugin } from '../puck/editor/experimentOutlinePlugin.js';
3
+ export { deriveAccent } from '../ui/accent.js';
4
+ export type { PuckRoutes } from '../puck/routes.js';
@@ -1,2 +1,4 @@
1
1
  'use client';
2
- export { ExperimentSlotEditor } from '../puck/editor/ExperimentSlotEditor.js';
2
+ export { ExperimentSlotEditor, createExperimentSlotEditor } from '../puck/editor/ExperimentSlotEditor.js';
3
+ export { experimentOutlinePlugin } from '../puck/editor/experimentOutlinePlugin.js';
4
+ export { deriveAccent } from '../ui/accent.js';
@@ -2,12 +2,17 @@ export { getAssignments } from '../server/getAssignments.js';
2
2
  export { resolveExperimentData } from '../server/resolveExperimentData.js';
3
3
  export { recordExposures } from '../server/recordExposures.js';
4
4
  export { getFlagPoint } from '../server/getFlagPoint.js';
5
- export { linkIdentity } from '../server/linkIdentity.js';
5
+ export { linkIdentity, type LinkIdentityOutcome } from '../server/linkIdentity.js';
6
6
  export { trackConversion } from '../server/trackConversion.js';
7
7
  export { eraseUser } from '../server/eraseUser.js';
8
+ export type { MetricUnavailableReason, ContentReportReason } from '../types/reasons.js';
9
+ export type { PreviewPlacement } from '../types/contracts.js';
8
10
  export { runResults } from '../server/runResults.js';
9
11
  export { recordContentChange } from '../server/recordContentChange.js';
10
12
  export { experimentSitemapFilter } from '../server/sitemap.js';
11
13
  export { experimentMetadata } from '../server/metadata.js';
12
14
  export { toClientAssignments } from '../server/toClientAssignments.js';
15
+ export { compileManifest } from '../manifest/compile.js';
16
+ export type { ManifestInputs, ManifestInputExperiment, ManifestPhase, ManifestCompileOptions } from '../manifest/compile.js';
17
+ export type { Manifest, ManifestExperiment } from '../types/manifest.js';
13
18
  export { contentChangesBeforeHook as captureContentBaseline } from '../hooks/contentChanges.js';
@@ -10,4 +10,6 @@ export { recordContentChange } from '../server/recordContentChange.js';
10
10
  export { experimentSitemapFilter } from '../server/sitemap.js';
11
11
  export { experimentMetadata } from '../server/metadata.js';
12
12
  export { toClientAssignments } from '../server/toClientAssignments.js';
13
+ // Pure manifest builder for host tests (CORE-43): the same compile transitions run, with no database access.
14
+ export { compileManifest } from '../manifest/compile.js';
13
15
  export { contentChangesBeforeHook as captureContentBaseline } from '../hooks/contentChanges.js';
@@ -0,0 +1,5 @@
1
+ import type { CollectionAfterOperationHook, CollectionBeforeChangeHook, CollectionBeforeDeleteHook, CollectionBeforeOperationHook } from 'payload';
2
+ export declare const pageGuardBeforeOperation: CollectionBeforeOperationHook;
3
+ export declare const pageGuardAfterOperation: CollectionAfterOperationHook;
4
+ export declare const pageGuardBeforeDelete: CollectionBeforeDeleteHook;
5
+ export declare const pageGuardBeforeChange: CollectionBeforeChangeHook;