@delmaredigital/payload-experiments 0.2.0-rc.6 → 0.3.0-rc.1

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 (291) hide show
  1. package/README.md +129 -11
  2. package/dist/admin/PreviewLinks.d.ts +2 -1
  3. package/dist/admin/PreviewLinks.js +24 -16
  4. package/dist/admin/actions/ConfirmDialog.d.ts +7 -4
  5. package/dist/admin/actions/ConfirmDialog.js +37 -82
  6. package/dist/admin/actions/DecideDialog.d.ts +3 -1
  7. package/dist/admin/actions/DecideDialog.js +30 -13
  8. package/dist/admin/actions/MakeContentResult.d.ts +13 -2
  9. package/dist/admin/actions/MakeContentResult.js +115 -16
  10. package/dist/admin/actions/actions.model.d.ts +2 -4
  11. package/dist/admin/actions/actions.model.js +2 -3
  12. package/dist/admin/allocation.model.js +3 -5
  13. package/dist/admin/api.d.ts +10 -4
  14. package/dist/admin/api.js +11 -7
  15. package/dist/admin/copy/actions.d.ts +39 -3
  16. package/dist/admin/copy/actions.js +40 -4
  17. package/dist/admin/copy/armSelect.js +5 -5
  18. package/dist/admin/copy/arms.d.ts +1 -7
  19. package/dist/admin/copy/arms.js +1 -7
  20. package/dist/admin/copy/audience.d.ts +2 -1
  21. package/dist/admin/copy/audience.js +3 -2
  22. package/dist/admin/copy/common.d.ts +13 -6
  23. package/dist/admin/copy/common.js +14 -8
  24. package/dist/admin/copy/goals.d.ts +21 -6
  25. package/dist/admin/copy/goals.js +27 -6
  26. package/dist/admin/copy/readiness.d.ts +19 -0
  27. package/dist/admin/copy/readiness.js +19 -0
  28. package/dist/admin/copy/recommendation.d.ts +0 -3
  29. package/dist/admin/copy/recommendation.js +0 -3
  30. package/dist/admin/copy/results.d.ts +41 -2
  31. package/dist/admin/copy/results.js +42 -3
  32. package/dist/admin/copy/review.d.ts +1 -1
  33. package/dist/admin/copy/review.js +1 -1
  34. package/dist/admin/copy/setting.d.ts +2 -0
  35. package/dist/admin/copy/setting.js +2 -0
  36. package/dist/admin/copy/surface.d.ts +7 -4
  37. package/dist/admin/copy/surface.js +9 -4
  38. package/dist/admin/copy/testPage.d.ts +12 -0
  39. package/dist/admin/copy/testPage.js +12 -0
  40. package/dist/admin/format/date.d.ts +2 -0
  41. package/dist/admin/format/date.js +10 -0
  42. package/dist/admin/labels.d.ts +2 -7
  43. package/dist/admin/labels.js +2 -7
  44. package/dist/admin/previewLinks.model.d.ts +2 -2
  45. package/dist/admin/previewLinks.model.js +5 -7
  46. package/dist/admin/results/CiChart.js +11 -21
  47. package/dist/admin/results/DataQualityPanel.d.ts +4 -2
  48. package/dist/admin/results/DataQualityPanel.js +30 -17
  49. package/dist/admin/results/DataTable.d.ts +16 -0
  50. package/dist/admin/results/DataTable.js +152 -0
  51. package/dist/admin/results/StatTile.d.ts +7 -0
  52. package/dist/admin/results/StatTile.js +22 -0
  53. package/dist/admin/results/VerdictHero.d.ts +13 -0
  54. package/dist/admin/results/{VerdictCard.js → VerdictHero.js} +15 -12
  55. package/dist/admin/results/releasePages.model.d.ts +17 -0
  56. package/dist/admin/results/releasePages.model.js +10 -0
  57. package/dist/admin/results/results.css +64 -0
  58. package/dist/admin/results/results.model.d.ts +6 -2
  59. package/dist/admin/results/results.model.js +12 -5
  60. package/dist/admin/surface/ResultsRoute.d.ts +1 -1
  61. package/dist/admin/surface/ResultsRoute.js +60 -31
  62. package/dist/admin/surface/SurfaceShell.d.ts +11 -2
  63. package/dist/admin/surface/SurfaceShell.js +49 -14
  64. package/dist/admin/surface/TestsHome.d.ts +6 -1
  65. package/dist/admin/surface/TestsHome.js +183 -112
  66. package/dist/admin/surface/TestsSurface.js +56 -16
  67. package/dist/admin/surface/contracts.d.ts +1 -0
  68. package/dist/admin/surface/goals/GoalForm.js +341 -308
  69. package/dist/admin/surface/goals/GoalsList.js +123 -89
  70. package/dist/admin/surface/goals/data.d.ts +8 -2
  71. package/dist/admin/surface/goals/data.js +11 -3
  72. package/dist/admin/surface/goals/goals.css +10 -13
  73. package/dist/admin/surface/home.model.d.ts +7 -8
  74. package/dist/admin/surface/home.model.js +23 -12
  75. package/dist/admin/surface/newTest/NewTest.js +5 -5
  76. package/dist/admin/surface/pageBlock/parts.js +31 -26
  77. package/dist/admin/surface/readiness/ActionDialogs.js +43 -14
  78. package/dist/admin/surface/readiness/Banner.d.ts +3 -0
  79. package/dist/admin/surface/readiness/Banner.js +63 -36
  80. package/dist/admin/surface/readiness/SaveStatus.d.ts +1 -0
  81. package/dist/admin/surface/readiness/SaveStatus.js +6 -3
  82. package/dist/admin/surface/readiness/readiness.model.d.ts +2 -0
  83. package/dist/admin/surface/readiness/readiness.model.js +18 -9
  84. package/dist/admin/surface/readiness/useTransition.d.ts +1 -0
  85. package/dist/admin/surface/readiness/useTransition.js +2 -1
  86. package/dist/admin/surface/results/ReleasePages.d.ts +14 -0
  87. package/dist/admin/surface/results/ReleasePages.js +116 -0
  88. package/dist/admin/surface/results/ResultsBody.d.ts +15 -2
  89. package/dist/admin/surface/results/ResultsBody.js +281 -107
  90. package/dist/admin/surface/results/ResultsHeader.d.ts +10 -0
  91. package/dist/admin/surface/results/ResultsHeader.js +177 -0
  92. package/dist/admin/surface/results/recommendation.model.js +2 -6
  93. package/dist/admin/surface/results/useResults.d.ts +21 -3
  94. package/dist/admin/surface/results/useResults.js +2 -1
  95. package/dist/admin/surface/setting/parts.js +75 -88
  96. package/dist/admin/surface/setting/setting.css +3 -15
  97. package/dist/admin/surface/surface.css +62 -47
  98. package/dist/admin/surface/test/AdvancedSection.js +55 -48
  99. package/dist/admin/surface/test/GoalSection.js +39 -32
  100. package/dist/admin/surface/test/LengthSection.js +46 -34
  101. package/dist/admin/surface/test/Rail.d.ts +4 -0
  102. package/dist/admin/surface/test/Rail.js +83 -0
  103. package/dist/admin/surface/test/ReadinessBar.d.ts +5 -0
  104. package/dist/admin/surface/test/ReadinessBar.js +33 -0
  105. package/dist/admin/surface/test/Section.d.ts +1 -1
  106. package/dist/admin/surface/test/Section.js +36 -16
  107. package/dist/admin/surface/test/TargetingSection.js +168 -142
  108. package/dist/admin/surface/test/TestPage.d.ts +2 -1
  109. package/dist/admin/surface/test/TestPage.js +146 -59
  110. package/dist/admin/surface/test/VersionsSection.js +151 -120
  111. package/dist/admin/surface/test/fields.d.ts +11 -4
  112. package/dist/admin/surface/test/fields.js +84 -85
  113. package/dist/admin/surface/test/jumpToSection.d.ts +3 -0
  114. package/dist/admin/surface/test/jumpToSection.js +10 -0
  115. package/dist/admin/surface/test/pageParts.js +9 -3
  116. package/dist/admin/surface/test/testPage.css +81 -14
  117. package/dist/admin/surface/useTestDocument.d.ts +1 -0
  118. package/dist/admin/surface/useTestDocument.js +40 -9
  119. package/dist/admin/targeting/targeting.model.d.ts +1 -3
  120. package/dist/admin/targeting/targeting.model.js +3 -8
  121. package/dist/admin/theme.css +115 -36
  122. package/dist/admin/theme.d.ts +38 -0
  123. package/dist/admin/theme.js +38 -1
  124. package/dist/admin/useApi.d.ts +17 -2
  125. package/dist/admin/views/ExperimentsRootView.js +12 -2
  126. package/dist/collections/experiments/config.js +44 -1
  127. package/dist/collections/experiments/lockHook.js +16 -2
  128. package/dist/collections/identities.js +6 -1
  129. package/dist/collections/metrics.js +7 -0
  130. package/dist/collections/metricsGuards.d.ts +9 -0
  131. package/dist/collections/metricsGuards.js +33 -0
  132. package/dist/constants.d.ts +1 -0
  133. package/dist/constants.js +1 -0
  134. package/dist/contentDone.d.ts +4 -0
  135. package/dist/contentDone.js +5 -0
  136. package/dist/db/identities.d.ts +8 -7
  137. package/dist/db/identities.js +9 -6
  138. package/dist/db/metricUsage.d.ts +19 -0
  139. package/dist/db/metricUsage.js +54 -0
  140. package/dist/db/names.d.ts +5 -0
  141. package/dist/db/names.js +5 -0
  142. package/dist/db/publishedContent.d.ts +10 -0
  143. package/dist/db/publishedContent.js +43 -0
  144. package/dist/db/results.d.ts +2 -0
  145. package/dist/db/results.js +6 -2
  146. package/dist/db/snapshots.d.ts +18 -6
  147. package/dist/db/snapshots.js +23 -7
  148. package/dist/db/staleEdit.d.ts +6 -0
  149. package/dist/db/staleEdit.js +10 -0
  150. package/dist/db/sweep.js +11 -6
  151. package/dist/endpoints/events.js +3 -3
  152. package/dist/endpoints/index.js +19 -1
  153. package/dist/endpoints/recompute.js +2 -1
  154. package/dist/endpoints/results.js +5 -0
  155. package/dist/endpoints/sanitizeReadinessDetail.d.ts +1 -0
  156. package/dist/endpoints/sanitizeReadinessDetail.js +2 -0
  157. package/dist/endpoints/summary.js +20 -2
  158. package/dist/endpoints/transitions.d.ts +15 -0
  159. package/dist/endpoints/transitions.js +36 -0
  160. package/dist/errors.d.ts +8 -0
  161. package/dist/errors.js +28 -0
  162. package/dist/exports/adminClient.d.ts +1 -0
  163. package/dist/exports/adminClient.js +1 -0
  164. package/dist/exports/index.d.ts +2 -1
  165. package/dist/exports/index.js +1 -0
  166. package/dist/exports/puckEditor.d.ts +1 -0
  167. package/dist/exports/puckEditor.js +1 -0
  168. package/dist/exports/rsc.d.ts +5 -1
  169. package/dist/exports/rsc.js +2 -0
  170. package/dist/hooks/hostHook.js +3 -2
  171. package/dist/hooks/pageGuard.d.ts +5 -0
  172. package/dist/hooks/pageGuard.js +121 -0
  173. package/dist/labels/arms.d.ts +7 -0
  174. package/dist/labels/arms.js +7 -0
  175. package/dist/log.d.ts +7 -0
  176. package/dist/log.js +38 -0
  177. package/dist/manifest/compile.d.ts +5 -0
  178. package/dist/manifest/compile.js +5 -1
  179. package/dist/options.js +23 -0
  180. package/dist/plugin.js +26 -2
  181. package/dist/puck/copy.d.ts +1 -1
  182. package/dist/puck/copy.js +6 -6
  183. package/dist/puck/editor/editor.chrome.css +235 -0
  184. package/dist/puck/editor/editor.css +241 -43
  185. package/dist/results/aggregate.js +4 -3
  186. package/dist/results/analyse.d.ts +4 -1
  187. package/dist/results/analyse.js +27 -11
  188. package/dist/results/cuped.js +3 -2
  189. package/dist/results/identityCredit.d.ts +15 -7
  190. package/dist/results/identityCredit.js +32 -33
  191. package/dist/results/rolloutUnits.js +2 -1
  192. package/dist/results/snapshot.js +8 -2
  193. package/dist/results/srm.js +2 -1
  194. package/dist/results/verdict.d.ts +2 -0
  195. package/dist/results/verdict.js +18 -1
  196. package/dist/sanitizeReadinessDetail.d.ts +7 -0
  197. package/dist/sanitizeReadinessDetail.js +54 -0
  198. package/dist/server/linkIdentity.d.ts +10 -1
  199. package/dist/server/linkIdentity.js +20 -18
  200. package/dist/server/metadata.d.ts +5 -0
  201. package/dist/server/metadata.js +9 -1
  202. package/dist/server/recordContentChange.js +2 -1
  203. package/dist/server/recordExposures.js +3 -3
  204. package/dist/server/runResults.d.ts +3 -2
  205. package/dist/server/runResults.js +125 -34
  206. package/dist/server/trackConversion.js +5 -6
  207. package/dist/targeting/values.d.ts +4 -0
  208. package/dist/targeting/values.js +10 -0
  209. package/dist/transitions/addMetric.js +8 -0
  210. package/dist/transitions/archive.js +2 -3
  211. package/dist/transitions/checkStart.d.ts +6 -3
  212. package/dist/transitions/checkStart.js +15 -0
  213. package/dist/transitions/contentFields.d.ts +7 -0
  214. package/dist/transitions/contentFields.js +20 -0
  215. package/dist/transitions/decide.js +6 -1
  216. package/dist/transitions/deleteExperiment.js +2 -1
  217. package/dist/transitions/dismissContentEntry.d.ts +8 -0
  218. package/dist/transitions/dismissContentEntry.js +45 -0
  219. package/dist/transitions/makeContent.d.ts +5 -2
  220. package/dist/transitions/makeContent.js +217 -118
  221. package/dist/transitions/phaseRecord.d.ts +8 -2
  222. package/dist/transitions/placements.d.ts +1 -0
  223. package/dist/transitions/placements.js +4 -0
  224. package/dist/transitions/preflightData.d.ts +1 -1
  225. package/dist/transitions/preflightData.js +42 -4
  226. package/dist/transitions/releasePages.d.ts +8 -0
  227. package/dist/transitions/releasePages.js +26 -0
  228. package/dist/transitions/restoreContentEntry.d.ts +8 -0
  229. package/dist/transitions/restoreContentEntry.js +47 -0
  230. package/dist/transitions/start.js +30 -5
  231. package/dist/types/contracts.d.ts +14 -0
  232. package/dist/types/endpoints.d.ts +34 -3
  233. package/dist/types/index.d.ts +1 -0
  234. package/dist/types/index.js +1 -0
  235. package/dist/types/reasons.d.ts +3 -0
  236. package/dist/types/reasons.js +1 -0
  237. package/dist/types/snapshot.d.ts +6 -1
  238. package/dist/ui/Banner.d.ts +2 -0
  239. package/dist/ui/Banner.js +2 -0
  240. package/dist/ui/Breadcrumb.d.ts +7 -0
  241. package/dist/ui/Breadcrumb.js +25 -0
  242. package/dist/ui/Button.d.ts +15 -0
  243. package/dist/ui/Button.js +32 -0
  244. package/dist/ui/Card.d.ts +12 -0
  245. package/dist/ui/Card.js +40 -0
  246. package/dist/ui/Checkbox.d.ts +2 -0
  247. package/dist/ui/Checkbox.js +2 -0
  248. package/dist/{admin/components → ui}/ChipsInput.d.ts +4 -3
  249. package/dist/{admin/components → ui}/ChipsInput.js +20 -12
  250. package/dist/ui/Dialog.d.ts +13 -0
  251. package/dist/ui/Dialog.js +85 -0
  252. package/dist/ui/EmptyState.d.ts +2 -0
  253. package/dist/ui/EmptyState.js +2 -0
  254. package/dist/ui/Feedback.d.ts +24 -0
  255. package/dist/ui/Feedback.js +40 -0
  256. package/dist/ui/Inputs.d.ts +34 -0
  257. package/dist/ui/Inputs.js +225 -0
  258. package/dist/ui/NumberInput.d.ts +2 -0
  259. package/dist/ui/NumberInput.js +2 -0
  260. package/dist/ui/Range.d.ts +2 -0
  261. package/dist/ui/Range.js +2 -0
  262. package/dist/ui/SearchInput.d.ts +2 -0
  263. package/dist/ui/SearchInput.js +2 -0
  264. package/dist/ui/SectionCard.d.ts +2 -0
  265. package/dist/ui/SectionCard.js +2 -0
  266. package/dist/ui/Select.d.ts +2 -0
  267. package/dist/ui/Select.js +2 -0
  268. package/dist/ui/StatusChip.d.ts +2 -0
  269. package/dist/ui/StatusChip.js +2 -0
  270. package/dist/ui/Tabs.d.ts +14 -0
  271. package/dist/ui/Tabs.js +54 -0
  272. package/dist/ui/TextInput.d.ts +2 -0
  273. package/dist/ui/TextInput.js +2 -0
  274. package/dist/ui/Textarea.d.ts +2 -0
  275. package/dist/ui/Textarea.js +2 -0
  276. package/dist/ui/Toggle.d.ts +2 -0
  277. package/dist/ui/Toggle.js +2 -0
  278. package/dist/ui/accent.d.ts +21 -0
  279. package/dist/ui/accent.js +81 -0
  280. package/dist/ui/index.d.ts +9 -0
  281. package/dist/ui/index.js +9 -0
  282. package/dist/ui/ui.css +92 -0
  283. package/package.json +1 -1
  284. package/dist/admin/components/chipsInput.css +0 -11
  285. package/dist/admin/results/ArmTable.d.ts +0 -8
  286. package/dist/admin/results/ArmTable.js +0 -113
  287. package/dist/admin/results/VerdictCard.d.ts +0 -9
  288. package/dist/admin/surface/goals/GoalsBreadcrumb.d.ts +0 -5
  289. package/dist/admin/surface/goals/GoalsBreadcrumb.js +0 -47
  290. package/dist/admin/surface/test/TestHeader.d.ts +0 -4
  291. package/dist/admin/surface/test/TestHeader.js +0 -69
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
  }
@@ -1,3 +1,4 @@
1
+ import { safeError } from '../log.js';
1
2
  import { NextRequest } from 'next/server.js';
2
3
  import { DEFAULT_MAX_BODY_BYTES } from '../constants.js';
3
4
  import { pooledExecutor } from '../db/executor.js';
@@ -89,9 +90,8 @@ export async function handleEvents(req, { options }) {
89
90
  } catch (error) {
90
91
  try {
91
92
  req.payload.logger.error({
92
- err: error,
93
- msg: 'Experiments: events endpoint failed'
94
- });
93
+ err: safeError(error)
94
+ }, 'Experiments: events endpoint failed');
95
95
  } catch {}
96
96
  }
97
97
  return accepted();
@@ -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',
@@ -1,3 +1,4 @@
1
+ import { safeError } from '../log.js';
1
2
  import { runResults } from '../server/runResults.js';
2
3
  import { readSnapshots, resultsError } from './results.js';
3
4
  import { routeId } from './routeId.js';
@@ -39,7 +40,7 @@ export async function handleRecompute(req, _ctx) {
39
40
  });
40
41
  } catch (error) {
41
42
  req.payload.logger.error({
42
- err: error,
43
+ err: safeError(error),
43
44
  experimentId: docs[0].id
44
45
  }, 'Experiment recompute failed');
45
46
  return resultsError(503, 'recompute-failed', 'Results could not be recomputed. Try again.');
@@ -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
+ export { XP_IF_UPDATED_AT_HEADER } from '../constants.js';
1
2
  export { LayerUsage } from '../admin/LayerUsage.js';
2
3
  export { MetricSourceSelect } from '../admin/MetricSourceSelect.js';
3
4
  export { ArmSelectField } from '../admin/armSelect/ArmSelectField.js';
@@ -1,4 +1,5 @@
1
1
  'use client';
2
+ export { XP_IF_UPDATED_AT_HEADER } from '../constants.js';
2
3
  export { LayerUsage } from '../admin/LayerUsage.js';
3
4
  export { MetricSourceSelect } from '../admin/MetricSourceSelect.js';
4
5
  export { ArmSelectField } from '../admin/armSelect/ArmSelectField.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, 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, 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 +1,2 @@
1
1
  export { ExperimentSlotEditor } from '../puck/editor/ExperimentSlotEditor.js';
2
+ export { deriveAccent } from '../ui/accent.js';
@@ -1,2 +1,3 @@
1
1
  'use client';
2
2
  export { ExperimentSlotEditor } from '../puck/editor/ExperimentSlotEditor.js';
3
+ export { deriveAccent } from '../ui/accent.js';
@@ -2,12 +2,16 @@ 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';
8
9
  export { runResults } from '../server/runResults.js';
9
10
  export { recordContentChange } from '../server/recordContentChange.js';
10
11
  export { experimentSitemapFilter } from '../server/sitemap.js';
11
12
  export { experimentMetadata } from '../server/metadata.js';
12
13
  export { toClientAssignments } from '../server/toClientAssignments.js';
14
+ export { compileManifest } from '../manifest/compile.js';
15
+ export type { ManifestInputs, ManifestInputExperiment, ManifestPhase, ManifestCompileOptions } from '../manifest/compile.js';
16
+ export type { Manifest, ManifestExperiment } from '../types/manifest.js';
13
17
  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';
@@ -1,9 +1,10 @@
1
+ import { safeError } from '../log.js';
1
2
  /** Host saves must survive synchronous and asynchronous plugin hook failures. */ export function neverThrow(name, hook) {
2
3
  return async (args)=>{
3
4
  try {
4
5
  return await hook(args);
5
6
  } catch (error) {
6
- console.error(`[payload-experiments] ${name} failed; the document was saved without it`, error);
7
+ console.error(`[payload-experiments] ${name} failed; the document was saved without it`, safeError(error));
7
8
  return args.doc;
8
9
  }
9
10
  };
@@ -13,7 +14,7 @@
13
14
  try {
14
15
  return await hook(args);
15
16
  } catch (error) {
16
- console.error(`[payload-experiments] ${name} failed; the document was saved without it`, error);
17
+ console.error(`[payload-experiments] ${name} failed; the document was saved without it`, safeError(error));
17
18
  return args.data;
18
19
  }
19
20
  };
@@ -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;
@@ -0,0 +1,121 @@
1
+ import { previousPublishedContent } from '../db/appends.js';
2
+ import { XpRefusal } from '../errors.js';
3
+ import { pageGuardCopy, READINESS_COPY } from '../admin/copy/readiness.js';
4
+ // createLocalReq replaces context with a shallow copy on nested calls. Keep
5
+ // the shared stack in that context so both the outer req and its proxies see it.
6
+ const operations = (req)=>req.context.xpPageGuardOperations;
7
+ export const pageGuardBeforeOperation = ({ args, operation })=>{
8
+ if (operation !== 'update' && operation !== 'restoreVersion') return;
9
+ const call = args;
10
+ let collections = operations(call.req);
11
+ if (!collections) call.req.context.xpPageGuardOperations = collections = new Map();
12
+ const slug = call.collection.config.slug;
13
+ const stack = collections.get(slug) ?? [];
14
+ stack.push({
15
+ draft: call.draft === true,
16
+ restore: operation === 'restoreVersion'
17
+ });
18
+ collections.set(slug, stack);
19
+ };
20
+ export const pageGuardAfterOperation = ({ req, collection, operation, result })=>{
21
+ // beforeOperation reports by-id updates as 'update'; afterOperation reports 'updateByID'.
22
+ if (operation === 'update' || operation === 'updateByID' || operation === 'restoreVersion') {
23
+ const collections = operations(req);
24
+ const stack = collections?.get(collection.slug);
25
+ stack?.pop();
26
+ if (!stack?.length) collections?.delete(collection.slug);
27
+ }
28
+ return result;
29
+ };
30
+ async function protect(req, path) {
31
+ // page guard: the hook must see test placements regardless of the editor's access
32
+ const tests = await req.payload.find({
33
+ collection: 'xp-experiments',
34
+ req,
35
+ overrideAccess: true,
36
+ depth: 0,
37
+ limit: 0,
38
+ where: {
39
+ and: [
40
+ {
41
+ kind: {
42
+ equals: 'page'
43
+ }
44
+ },
45
+ {
46
+ status: {
47
+ in: [
48
+ 'running',
49
+ 'decided',
50
+ 'archived'
51
+ ]
52
+ }
53
+ }
54
+ ]
55
+ }
56
+ });
57
+ for (const test of tests.docs){
58
+ const decision = test.decision;
59
+ const archived = test.status === 'archived';
60
+ if (archived && (!decision?.winnerArm || decision.discardedAt || decision.pagesReleasedAt)) continue;
61
+ // page guard: the hook must see test placements regardless of the editor's access
62
+ const phases = await req.payload.find({
63
+ collection: 'xp-phases',
64
+ req,
65
+ overrideAccess: true,
66
+ depth: 0,
67
+ limit: 1,
68
+ sort: '-phase',
69
+ where: {
70
+ experiment: {
71
+ equals: test.id
72
+ },
73
+ phaseKind: {
74
+ equals: 'test'
75
+ }
76
+ }
77
+ });
78
+ const phase = phases.docs[0];
79
+ if (!phase) throw new XpRefusal('invalid-phase', READINESS_COPY.missingVersions, {
80
+ experimentId: test.id
81
+ }, 409);
82
+ const arms = phase.arms;
83
+ const protectedArms = archived ? arms.filter((arm)=>arm.kind === 'control' || arm.key === decision.winnerArm) : arms;
84
+ if (protectedArms.some((arm)=>arm.pageTarget === path)) {
85
+ throw new XpRefusal('locked-placement', pageGuardCopy(String(test.name), test.status), {
86
+ experimentId: test.id
87
+ }, 409);
88
+ }
89
+ }
90
+ }
91
+ function registration(req, slug) {
92
+ return (req.payload.config.custom?.xp).pages?.collections.find((entry)=>entry.slug === slug);
93
+ }
94
+ export const pageGuardBeforeDelete = async ({ req, id, collection })=>{
95
+ const page = registration(req, collection.slug);
96
+ if (!page) return;
97
+ const published = await previousPublishedContent(req, collection.slug, id);
98
+ if (!published || published._status === 'draft') return;
99
+ const path = page.pathOf(published);
100
+ if (path !== null) await protect(req, path);
101
+ };
102
+ export const pageGuardBeforeChange = async ({ req, collection, originalDoc, operation, data })=>{
103
+ if (operation !== 'update') return data;
104
+ const page = registration(req, collection.slug);
105
+ if (!page) return data;
106
+ const drafts = Boolean(collection.versions && collection.versions.drafts);
107
+ // updateDocument computes isSavingDraft from draftArg and _status before
108
+ // beforeChange, and writes the main row only when !isSavingDraft.
109
+ const call = operations(req)?.get(collection.slug)?.at(-1);
110
+ if (drafts && call?.draft && (call.restore || data._status !== 'published')) return data;
111
+ const published = await previousPublishedContent(req, collection.slug, originalDoc.id);
112
+ if (!published || published._status === 'draft') return data;
113
+ const path = page.pathOf(published);
114
+ const unpublishing = drafts && data._status === 'draft';
115
+ const trashing = collection.trash && data.deletedAt != null && !originalDoc.deletedAt;
116
+ if (path !== null && (trashing || unpublishing || page.pathOf({
117
+ ...originalDoc,
118
+ ...data
119
+ }) !== path)) await protect(req, path);
120
+ return data;
121
+ };
@@ -0,0 +1,7 @@
1
+ export declare const ARM_LABELS: {
2
+ readonly control: "Original";
3
+ readonly variantA: "Variant A";
4
+ readonly variantB: "Variant B";
5
+ readonly variantC: "Variant C";
6
+ readonly holdout: "Holdout";
7
+ };
@@ -0,0 +1,7 @@
1
+ export const ARM_LABELS = {
2
+ control: 'Original',
3
+ variantA: 'Variant A',
4
+ variantB: 'Variant B',
5
+ variantC: 'Variant C',
6
+ holdout: 'Holdout'
7
+ };
package/dist/log.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ /** Deliberately construct a plain object: host serializers must never see the error. */
2
+ export declare function safeError(err: unknown): {
3
+ name: string;
4
+ code?: string;
5
+ stack?: string;
6
+ message?: string;
7
+ };
package/dist/log.js ADDED
@@ -0,0 +1,38 @@
1
+ import { ANALYSIS_ERROR_MESSAGES, XpAnalysisError, XpNoTransactionError, XpNotImplemented, XpRefusal } from './errors.js';
2
+ import { ResultsBudgetError } from './db/pgExecutor.js';
3
+ /** Deliberately construct a plain object: host serializers must never see the error. */ export function safeError(err) {
4
+ try {
5
+ if (!(err instanceof Error)) return {
6
+ name: typeof err
7
+ };
8
+ const name = err.name;
9
+ const result = {
10
+ name: typeof name === 'string' ? name : 'Error'
11
+ };
12
+ const code = 'code' in err ? err.code : undefined;
13
+ const analysisCode = err instanceof XpAnalysisError && typeof code === 'string' && Object.hasOwn(ANALYSIS_ERROR_MESSAGES, code) ? code : undefined;
14
+ if (typeof code === 'string' && (/^[0-9A-Z]{5}$/.test(code) || err instanceof XpRefusal || analysisCode)) result.code = code;
15
+ const message = err.message;
16
+ const stack = err.stack;
17
+ // Remove the entire V8 header first: message lines can impersonate frames.
18
+ // Unknown stack formats fail closed. Never traverse cause or database fields.
19
+ if (typeof name === 'string' && typeof message === 'string' && typeof stack === 'string') {
20
+ const header = message ? `${name}: ${message}` : name;
21
+ const frames = stack.startsWith(`${header}\n`) ? stack.slice(header.length + 1).split('\n').filter((line)=>/^\s+at /.test(line)) : [];
22
+ if (frames.length) result.stack = frames.join('\n');
23
+ }
24
+ // XpRefusal includes stored slugs; XpLockedFieldError includes request keys
25
+ // and stored status. Neither class's message is safe to send to host logs.
26
+ // An analysis error's logged text always comes from the fixed table by code, never from err.message.
27
+ if (analysisCode) result.message = ANALYSIS_ERROR_MESSAGES[analysisCode];
28
+ else if (err instanceof XpNoTransactionError || err instanceof XpNotImplemented || err instanceof ResultsBudgetError) {
29
+ if (typeof message === 'string') result.message = message;
30
+ }
31
+ return result;
32
+ } catch {
33
+ // Even instanceof, property existence checks and getters can throw on hosts' errors.
34
+ return {
35
+ name: 'Error'
36
+ };
37
+ }
38
+ }
@@ -35,6 +35,7 @@ export interface ManifestInputExperiment {
35
35
  winnerArm?: string | null;
36
36
  discardedAt?: string | null;
37
37
  holdoutReleasedAt?: string | null;
38
+ pagesReleasedAt?: string | null;
38
39
  } | null;
39
40
  currentPhase: ManifestPhase | null;
40
41
  lastTestPhase?: ManifestPhase | null;
@@ -50,6 +51,10 @@ export declare function loadManifestInputs(req: PayloadRequest, { excludeIds, pr
50
51
  pruneFlagDefaultIds?: DocId[];
51
52
  }): Promise<ManifestInputs>;
52
53
  /** Pure compiler. Assignment config comes exclusively from the frozen phase. */
54
+ /**
55
+ * Pure manifest builder (exported from `/rsc` for host tests). Pass the same `attributeKeys` as the plugin options,
56
+ * or legacy untyped targeting rules are dropped. Its input shape may change in a pre-1.0 minor.
57
+ */
53
58
  export declare function compileManifest(inputs: ManifestInputs, revision: number, { attributeKeys, onInvalid }?: ManifestCompileOptions): Manifest;
54
59
  /** Take the state lock before reading inputs. Under READ COMMITTED, a builder
55
60
  * waiting for another transition then sees that transition's committed changes,
@@ -107,7 +107,10 @@ export async function loadManifestInputs(req, { excludeIds = [], pruneFlagDefaul
107
107
  });
108
108
  inputs.flagDefaults = defaults;
109
109
  }
110
- /** Pure compiler. Assignment config comes exclusively from the frozen phase. */ export function compileManifest(inputs, revision, { attributeKeys, onInvalid } = {}) {
110
+ /** Pure compiler. Assignment config comes exclusively from the frozen phase. */ /**
111
+ * Pure manifest builder (exported from `/rsc` for host tests). Pass the same `attributeKeys` as the plugin options,
112
+ * or legacy untyped targeting rules are dropped. Its input shape may change in a pre-1.0 minor.
113
+ */ export function compileManifest(inputs, revision, { attributeKeys, onInvalid } = {}) {
111
114
  const excluded = new Set(inputs.excludeIds?.map(String));
112
115
  const prunedDefaults = new Set(inputs.pruneFlagDefaultIds?.map(String));
113
116
  const manifest = {
@@ -126,6 +129,7 @@ export async function loadManifestInputs(req, { excludeIds = [], pruneFlagDefaul
126
129
  const { decision, currentPhase: phase } = experiment;
127
130
  if (experiment.status === 'archived') {
128
131
  if (!decision?.winnerArm || decision.discardedAt) continue;
132
+ if (experiment.kind === 'page' && decision.pagesReleasedAt) continue;
129
133
  const testPhase = experiment.lastTestPhase ?? (phase?.phaseKind === 'test' ? phase : null);
130
134
  if (!testPhase) throw new XpRefusal('invalid-status', `Missing last test phase for experiment ${experiment.slug}`, undefined, 409);
131
135
  const winner = testPhase.arms.find((arm)=>arm.key === decision.winnerArm);