@delmaredigital/payload-experiments 0.2.0-rc.7 → 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 (267) 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/audience.d.ts +2 -1
  19. package/dist/admin/copy/audience.js +3 -2
  20. package/dist/admin/copy/common.d.ts +13 -6
  21. package/dist/admin/copy/common.js +14 -8
  22. package/dist/admin/copy/goals.d.ts +21 -6
  23. package/dist/admin/copy/goals.js +27 -6
  24. package/dist/admin/copy/readiness.d.ts +19 -0
  25. package/dist/admin/copy/readiness.js +19 -0
  26. package/dist/admin/copy/recommendation.d.ts +0 -3
  27. package/dist/admin/copy/recommendation.js +0 -3
  28. package/dist/admin/copy/results.d.ts +41 -2
  29. package/dist/admin/copy/results.js +42 -3
  30. package/dist/admin/copy/review.d.ts +1 -1
  31. package/dist/admin/copy/review.js +1 -1
  32. package/dist/admin/copy/setting.d.ts +2 -0
  33. package/dist/admin/copy/setting.js +2 -0
  34. package/dist/admin/copy/surface.d.ts +7 -4
  35. package/dist/admin/copy/surface.js +9 -4
  36. package/dist/admin/copy/testPage.d.ts +10 -0
  37. package/dist/admin/copy/testPage.js +10 -0
  38. package/dist/admin/format/date.d.ts +2 -0
  39. package/dist/admin/format/date.js +10 -0
  40. package/dist/admin/previewLinks.model.d.ts +2 -2
  41. package/dist/admin/previewLinks.model.js +5 -7
  42. package/dist/admin/results/CiChart.js +11 -21
  43. package/dist/admin/results/DataQualityPanel.d.ts +4 -2
  44. package/dist/admin/results/DataQualityPanel.js +30 -17
  45. package/dist/admin/results/DataTable.d.ts +16 -0
  46. package/dist/admin/results/DataTable.js +152 -0
  47. package/dist/admin/results/StatTile.d.ts +7 -0
  48. package/dist/admin/results/StatTile.js +22 -0
  49. package/dist/admin/results/VerdictHero.d.ts +13 -0
  50. package/dist/admin/results/{VerdictCard.js → VerdictHero.js} +15 -12
  51. package/dist/admin/results/releasePages.model.d.ts +17 -0
  52. package/dist/admin/results/releasePages.model.js +10 -0
  53. package/dist/admin/results/results.css +64 -0
  54. package/dist/admin/results/results.model.d.ts +5 -1
  55. package/dist/admin/results/results.model.js +10 -2
  56. package/dist/admin/surface/ResultsRoute.d.ts +1 -1
  57. package/dist/admin/surface/ResultsRoute.js +59 -34
  58. package/dist/admin/surface/SurfaceShell.d.ts +11 -2
  59. package/dist/admin/surface/SurfaceShell.js +49 -14
  60. package/dist/admin/surface/TestsHome.d.ts +6 -1
  61. package/dist/admin/surface/TestsHome.js +183 -112
  62. package/dist/admin/surface/TestsSurface.js +56 -16
  63. package/dist/admin/surface/goals/GoalForm.js +341 -308
  64. package/dist/admin/surface/goals/GoalsList.js +123 -89
  65. package/dist/admin/surface/goals/data.d.ts +8 -2
  66. package/dist/admin/surface/goals/data.js +11 -3
  67. package/dist/admin/surface/goals/goals.css +10 -13
  68. package/dist/admin/surface/home.model.d.ts +7 -8
  69. package/dist/admin/surface/home.model.js +23 -12
  70. package/dist/admin/surface/newTest/NewTest.js +5 -5
  71. package/dist/admin/surface/pageBlock/parts.js +31 -26
  72. package/dist/admin/surface/readiness/ActionDialogs.js +43 -14
  73. package/dist/admin/surface/readiness/Banner.d.ts +3 -0
  74. package/dist/admin/surface/readiness/Banner.js +58 -33
  75. package/dist/admin/surface/readiness/SaveStatus.d.ts +1 -0
  76. package/dist/admin/surface/readiness/SaveStatus.js +6 -3
  77. package/dist/admin/surface/readiness/readiness.model.d.ts +2 -0
  78. package/dist/admin/surface/readiness/readiness.model.js +18 -9
  79. package/dist/admin/surface/readiness/useTransition.d.ts +1 -0
  80. package/dist/admin/surface/readiness/useTransition.js +2 -1
  81. package/dist/admin/surface/results/ReleasePages.d.ts +14 -0
  82. package/dist/admin/surface/results/ReleasePages.js +116 -0
  83. package/dist/admin/surface/results/ResultsBody.d.ts +11 -1
  84. package/dist/admin/surface/results/ResultsBody.js +276 -105
  85. package/dist/admin/surface/results/ResultsHeader.d.ts +10 -0
  86. package/dist/admin/surface/results/ResultsHeader.js +177 -0
  87. package/dist/admin/surface/results/recommendation.model.js +2 -6
  88. package/dist/admin/surface/results/useResults.d.ts +21 -3
  89. package/dist/admin/surface/results/useResults.js +2 -1
  90. package/dist/admin/surface/setting/parts.js +75 -88
  91. package/dist/admin/surface/setting/setting.css +3 -15
  92. package/dist/admin/surface/surface.css +62 -47
  93. package/dist/admin/surface/test/AdvancedSection.js +55 -48
  94. package/dist/admin/surface/test/GoalSection.js +39 -32
  95. package/dist/admin/surface/test/LengthSection.js +46 -34
  96. package/dist/admin/surface/test/Rail.d.ts +4 -0
  97. package/dist/admin/surface/test/Rail.js +83 -0
  98. package/dist/admin/surface/test/ReadinessBar.d.ts +5 -0
  99. package/dist/admin/surface/test/ReadinessBar.js +33 -0
  100. package/dist/admin/surface/test/Section.d.ts +1 -1
  101. package/dist/admin/surface/test/Section.js +36 -16
  102. package/dist/admin/surface/test/TargetingSection.js +168 -142
  103. package/dist/admin/surface/test/TestPage.d.ts +2 -1
  104. package/dist/admin/surface/test/TestPage.js +146 -68
  105. package/dist/admin/surface/test/VersionsSection.js +151 -120
  106. package/dist/admin/surface/test/fields.d.ts +11 -4
  107. package/dist/admin/surface/test/fields.js +84 -85
  108. package/dist/admin/surface/test/jumpToSection.d.ts +3 -0
  109. package/dist/admin/surface/test/jumpToSection.js +10 -0
  110. package/dist/admin/surface/test/pageParts.js +9 -3
  111. package/dist/admin/surface/test/testPage.css +81 -14
  112. package/dist/admin/surface/useTestDocument.js +10 -4
  113. package/dist/admin/targeting/targeting.model.d.ts +1 -3
  114. package/dist/admin/targeting/targeting.model.js +3 -8
  115. package/dist/admin/theme.css +115 -36
  116. package/dist/admin/theme.d.ts +38 -0
  117. package/dist/admin/theme.js +38 -1
  118. package/dist/admin/useApi.d.ts +17 -2
  119. package/dist/admin/views/ExperimentsRootView.js +12 -2
  120. package/dist/collections/experiments/config.js +25 -0
  121. package/dist/collections/experiments/lockHook.js +3 -1
  122. package/dist/collections/identities.js +6 -1
  123. package/dist/collections/metrics.js +7 -0
  124. package/dist/collections/metricsGuards.d.ts +9 -0
  125. package/dist/collections/metricsGuards.js +33 -0
  126. package/dist/contentDone.d.ts +4 -0
  127. package/dist/contentDone.js +5 -0
  128. package/dist/db/identities.d.ts +8 -7
  129. package/dist/db/identities.js +9 -6
  130. package/dist/db/metricUsage.d.ts +19 -0
  131. package/dist/db/metricUsage.js +54 -0
  132. package/dist/db/names.d.ts +5 -0
  133. package/dist/db/names.js +5 -0
  134. package/dist/db/publishedContent.d.ts +10 -0
  135. package/dist/db/publishedContent.js +43 -0
  136. package/dist/db/results.d.ts +2 -0
  137. package/dist/db/results.js +6 -2
  138. package/dist/db/snapshots.d.ts +18 -6
  139. package/dist/db/snapshots.js +23 -7
  140. package/dist/db/sweep.js +11 -6
  141. package/dist/endpoints/index.js +19 -1
  142. package/dist/endpoints/results.js +5 -0
  143. package/dist/endpoints/sanitizeReadinessDetail.d.ts +1 -0
  144. package/dist/endpoints/sanitizeReadinessDetail.js +2 -0
  145. package/dist/endpoints/summary.js +20 -2
  146. package/dist/endpoints/transitions.d.ts +15 -0
  147. package/dist/endpoints/transitions.js +36 -0
  148. package/dist/errors.d.ts +8 -0
  149. package/dist/errors.js +28 -0
  150. package/dist/exports/index.d.ts +2 -1
  151. package/dist/exports/index.js +1 -0
  152. package/dist/exports/puckEditor.d.ts +1 -0
  153. package/dist/exports/puckEditor.js +1 -0
  154. package/dist/exports/rsc.d.ts +5 -1
  155. package/dist/exports/rsc.js +2 -0
  156. package/dist/hooks/pageGuard.d.ts +5 -0
  157. package/dist/hooks/pageGuard.js +121 -0
  158. package/dist/log.js +6 -3
  159. package/dist/manifest/compile.d.ts +5 -0
  160. package/dist/manifest/compile.js +5 -1
  161. package/dist/options.js +23 -0
  162. package/dist/plugin.js +26 -2
  163. package/dist/puck/copy.d.ts +1 -1
  164. package/dist/puck/copy.js +1 -1
  165. package/dist/puck/editor/editor.chrome.css +235 -0
  166. package/dist/puck/editor/editor.css +241 -43
  167. package/dist/results/aggregate.js +4 -3
  168. package/dist/results/analyse.d.ts +4 -1
  169. package/dist/results/analyse.js +27 -11
  170. package/dist/results/cuped.js +3 -2
  171. package/dist/results/identityCredit.d.ts +15 -7
  172. package/dist/results/identityCredit.js +32 -33
  173. package/dist/results/rolloutUnits.js +2 -1
  174. package/dist/results/snapshot.js +8 -2
  175. package/dist/results/srm.js +2 -1
  176. package/dist/sanitizeReadinessDetail.d.ts +7 -0
  177. package/dist/sanitizeReadinessDetail.js +54 -0
  178. package/dist/server/linkIdentity.d.ts +10 -1
  179. package/dist/server/linkIdentity.js +17 -15
  180. package/dist/server/metadata.d.ts +5 -0
  181. package/dist/server/metadata.js +9 -1
  182. package/dist/server/runResults.d.ts +3 -2
  183. package/dist/server/runResults.js +121 -31
  184. package/dist/targeting/values.d.ts +4 -0
  185. package/dist/targeting/values.js +10 -0
  186. package/dist/transitions/addMetric.js +8 -0
  187. package/dist/transitions/archive.js +2 -3
  188. package/dist/transitions/checkStart.d.ts +6 -3
  189. package/dist/transitions/checkStart.js +15 -0
  190. package/dist/transitions/contentFields.d.ts +7 -0
  191. package/dist/transitions/contentFields.js +20 -0
  192. package/dist/transitions/decide.js +6 -1
  193. package/dist/transitions/dismissContentEntry.d.ts +8 -0
  194. package/dist/transitions/dismissContentEntry.js +45 -0
  195. package/dist/transitions/makeContent.d.ts +5 -2
  196. package/dist/transitions/makeContent.js +217 -118
  197. package/dist/transitions/phaseRecord.d.ts +8 -2
  198. package/dist/transitions/placements.d.ts +1 -0
  199. package/dist/transitions/placements.js +4 -0
  200. package/dist/transitions/preflightData.d.ts +1 -1
  201. package/dist/transitions/preflightData.js +42 -4
  202. package/dist/transitions/releasePages.d.ts +8 -0
  203. package/dist/transitions/releasePages.js +26 -0
  204. package/dist/transitions/restoreContentEntry.d.ts +8 -0
  205. package/dist/transitions/restoreContentEntry.js +47 -0
  206. package/dist/transitions/start.js +30 -5
  207. package/dist/types/contracts.d.ts +14 -0
  208. package/dist/types/endpoints.d.ts +34 -3
  209. package/dist/types/index.d.ts +1 -0
  210. package/dist/types/index.js +1 -0
  211. package/dist/types/reasons.d.ts +3 -0
  212. package/dist/types/reasons.js +1 -0
  213. package/dist/types/snapshot.d.ts +6 -1
  214. package/dist/ui/Banner.d.ts +2 -0
  215. package/dist/ui/Banner.js +2 -0
  216. package/dist/ui/Breadcrumb.d.ts +7 -0
  217. package/dist/ui/Breadcrumb.js +25 -0
  218. package/dist/ui/Button.d.ts +15 -0
  219. package/dist/ui/Button.js +32 -0
  220. package/dist/ui/Card.d.ts +12 -0
  221. package/dist/ui/Card.js +40 -0
  222. package/dist/ui/Checkbox.d.ts +2 -0
  223. package/dist/ui/Checkbox.js +2 -0
  224. package/dist/{admin/components → ui}/ChipsInput.d.ts +4 -3
  225. package/dist/{admin/components → ui}/ChipsInput.js +20 -12
  226. package/dist/ui/Dialog.d.ts +13 -0
  227. package/dist/ui/Dialog.js +85 -0
  228. package/dist/ui/EmptyState.d.ts +2 -0
  229. package/dist/ui/EmptyState.js +2 -0
  230. package/dist/ui/Feedback.d.ts +24 -0
  231. package/dist/ui/Feedback.js +40 -0
  232. package/dist/ui/Inputs.d.ts +34 -0
  233. package/dist/ui/Inputs.js +225 -0
  234. package/dist/ui/NumberInput.d.ts +2 -0
  235. package/dist/ui/NumberInput.js +2 -0
  236. package/dist/ui/Range.d.ts +2 -0
  237. package/dist/ui/Range.js +2 -0
  238. package/dist/ui/SearchInput.d.ts +2 -0
  239. package/dist/ui/SearchInput.js +2 -0
  240. package/dist/ui/SectionCard.d.ts +2 -0
  241. package/dist/ui/SectionCard.js +2 -0
  242. package/dist/ui/Select.d.ts +2 -0
  243. package/dist/ui/Select.js +2 -0
  244. package/dist/ui/StatusChip.d.ts +2 -0
  245. package/dist/ui/StatusChip.js +2 -0
  246. package/dist/ui/Tabs.d.ts +14 -0
  247. package/dist/ui/Tabs.js +54 -0
  248. package/dist/ui/TextInput.d.ts +2 -0
  249. package/dist/ui/TextInput.js +2 -0
  250. package/dist/ui/Textarea.d.ts +2 -0
  251. package/dist/ui/Textarea.js +2 -0
  252. package/dist/ui/Toggle.d.ts +2 -0
  253. package/dist/ui/Toggle.js +2 -0
  254. package/dist/ui/accent.d.ts +21 -0
  255. package/dist/ui/accent.js +81 -0
  256. package/dist/ui/index.d.ts +9 -0
  257. package/dist/ui/index.js +9 -0
  258. package/dist/ui/ui.css +92 -0
  259. package/package.json +1 -1
  260. package/dist/admin/components/chipsInput.css +0 -11
  261. package/dist/admin/results/ArmTable.d.ts +0 -8
  262. package/dist/admin/results/ArmTable.js +0 -113
  263. package/dist/admin/results/VerdictCard.d.ts +0 -9
  264. package/dist/admin/surface/goals/GoalsBreadcrumb.d.ts +0 -5
  265. package/dist/admin/surface/goals/GoalsBreadcrumb.js +0 -47
  266. package/dist/admin/surface/test/TestHeader.d.ts +0 -4
  267. package/dist/admin/surface/test/TestHeader.js +0 -70
@@ -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,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';
@@ -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
+ };
package/dist/log.js CHANGED
@@ -1,4 +1,4 @@
1
- import { XpNoTransactionError, XpNotImplemented, XpRefusal } from './errors.js';
1
+ import { ANALYSIS_ERROR_MESSAGES, XpAnalysisError, XpNoTransactionError, XpNotImplemented, XpRefusal } from './errors.js';
2
2
  import { ResultsBudgetError } from './db/pgExecutor.js';
3
3
  /** Deliberately construct a plain object: host serializers must never see the error. */ export function safeError(err) {
4
4
  try {
@@ -10,7 +10,8 @@ import { ResultsBudgetError } from './db/pgExecutor.js';
10
10
  name: typeof name === 'string' ? name : 'Error'
11
11
  };
12
12
  const code = 'code' in err ? err.code : undefined;
13
- if (typeof code === 'string' && (/^[0-9A-Z]{5}$/.test(code) || err instanceof XpRefusal)) result.code = code;
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;
14
15
  const message = err.message;
15
16
  const stack = err.stack;
16
17
  // Remove the entire V8 header first: message lines can impersonate frames.
@@ -22,7 +23,9 @@ import { ResultsBudgetError } from './db/pgExecutor.js';
22
23
  }
23
24
  // XpRefusal includes stored slugs; XpLockedFieldError includes request keys
24
25
  // and stored status. Neither class's message is safe to send to host logs.
25
- if (err instanceof XpNoTransactionError || err instanceof XpNotImplemented || err instanceof ResultsBudgetError) {
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) {
26
29
  if (typeof message === 'string') result.message = message;
27
30
  }
28
31
  return result;
@@ -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);
package/dist/options.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { contrast, deriveAccent } from './ui/accent.js';
1
2
  import { DEFAULT_MAX_BODY_BYTES } from './constants.js';
2
3
  const normalisedCollections = new WeakMap();
3
4
  /** Resolve once per registration list; every consumer shares the same field defaults. */ export function puckCollections(options) {
@@ -54,6 +55,28 @@ export function resolveOptions(input) {
54
55
  }
55
56
  }
56
57
  }
58
+ if (input.pages !== undefined) {
59
+ if (!input.pages || !Array.isArray(input.pages.collections) || input.pages.collections.length === 0) throw new Error('Experiments pages.collections must be a non-empty array');
60
+ const slugs = new Set();
61
+ for (const entry of input.pages.collections){
62
+ if (!entry || typeof entry.slug !== 'string' || !entry.slug.trim() || slugs.has(entry.slug) || typeof entry.pathOf !== 'function' || typeof entry.findByPath !== 'function') {
63
+ throw new Error('Experiments pages.collections requires unique slugs, pathOf and findByPath functions');
64
+ }
65
+ slugs.add(entry.slug);
66
+ }
67
+ }
68
+ if (input.theme !== undefined) {
69
+ if (!input.theme || typeof input.theme !== 'object' || Array.isArray(input.theme)) throw new Error('Experiments theme must be an object');
70
+ for (const key of Object.keys(input.theme))if (key !== 'accent') throw new Error(`Unknown experiments theme option: ${key}`);
71
+ const accent = input.theme.accent;
72
+ if (accent !== undefined && accent !== '') {
73
+ if (typeof accent !== 'string' || !/^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i.test(accent)) throw new Error('Experiments theme.accent must be a CSS hex colour (#rgb or #rrggbb)');
74
+ for (const [mode, set] of Object.entries(deriveAccent(accent))){
75
+ const ratio = contrast(set.accent, set.fg);
76
+ if (ratio < 4.5) throw new Error(`Experiments theme.accent ${accent}: ${mode} foreground contrast ${ratio.toFixed(2)}:1 is below 4.5:1`);
77
+ }
78
+ }
79
+ }
57
80
  const ids = new Set();
58
81
  for (const point of input.flagPoints ?? []){
59
82
  if (typeof point.id !== 'string' || !point.id || ids.has(point.id)) throw new Error('Experiments flag point ids must be unique nonempty strings');
package/dist/plugin.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { buildCollections } from './collections/index.js';
2
2
  import { buildEndpoints } from './endpoints/index.js';
3
3
  import { contentChangesBeforeHook, contentChangesHook } from './hooks/contentChanges.js';
4
+ import { pageGuardAfterOperation, pageGuardBeforeChange, pageGuardBeforeDelete, pageGuardBeforeOperation } from './hooks/pageGuard.js';
4
5
  import { createOnInit } from './onInit.js';
5
6
  import { puckCollections, resolveOptions } from './options.js';
6
7
  import * as paths from './admin/paths.js';
@@ -11,6 +12,9 @@ export function createExperimentsPlugin(input) {
11
12
  for (const { slug } of registrations){
12
13
  if (!config.collections?.some((collection)=>collection.slug === slug)) throw new Error(`Unknown experiments puck collection: ${slug}`);
13
14
  }
15
+ for (const { slug } of options.pages?.collections ?? []){
16
+ if (!config.collections?.some((collection)=>collection.slug === slug)) throw new Error(`Unknown experiments page collection: ${slug}`);
17
+ }
14
18
  // The frozen factories consume registration/access metadata, never TUser callbacks.
15
19
  // Erase the user subtype only at this non-generic factory boundary.
16
20
  const { collections, globals } = buildCollections(options);
@@ -50,15 +54,35 @@ export function createExperimentsPlugin(input) {
50
54
  collections: [
51
55
  ...(config.collections ?? []).map((collection)=>{
52
56
  const puck = options.puck?.enabled && registrations.some((entry)=>entry.slug === collection.slug);
53
- if (!puck) return collection;
57
+ const page = options.pages?.collections.some((entry)=>entry.slug === collection.slug);
58
+ if (!puck && !page) return collection;
54
59
  return {
55
60
  ...collection,
56
61
  hooks: {
57
62
  ...collection.hooks,
58
63
  beforeChange: [
59
64
  ...collection.hooks?.beforeChange ?? [],
60
- contentChangesBeforeHook
65
+ ...page ? [
66
+ pageGuardBeforeChange
67
+ ] : [],
68
+ ...puck ? [
69
+ contentChangesBeforeHook
70
+ ] : []
61
71
  ],
72
+ ...page ? {
73
+ beforeOperation: [
74
+ ...collection.hooks?.beforeOperation ?? [],
75
+ pageGuardBeforeOperation
76
+ ],
77
+ beforeDelete: [
78
+ ...collection.hooks?.beforeDelete ?? [],
79
+ pageGuardBeforeDelete
80
+ ],
81
+ afterOperation: [
82
+ pageGuardAfterOperation,
83
+ ...collection.hooks?.afterOperation ?? []
84
+ ]
85
+ } : {},
62
86
  ...puck ? {
63
87
  afterChange: [
64
88
  ...collection.hooks?.afterChange ?? [],
@@ -9,7 +9,7 @@ export declare const pickerCopy: {
9
9
  readonly none: "No experiment";
10
10
  readonly loading: "Loading experiments…";
11
11
  readonly error: "Couldn't load experiments.";
12
- readonly forbidden: "You can't see experiments. Ask an admin.";
12
+ readonly forbidden: "You can't see tests. Ask an admin.";
13
13
  readonly statuses: {
14
14
  readonly draft: "Draft";
15
15
  readonly running: "Running";
package/dist/puck/copy.js CHANGED
@@ -10,7 +10,7 @@ export const pickerCopy = {
10
10
  none: 'No experiment',
11
11
  loading: 'Loading experiments…',
12
12
  error: "Couldn't load experiments.",
13
- forbidden: "You can't see experiments. Ask an admin.",
13
+ forbidden: "You can't see tests. Ask an admin.",
14
14
  statuses: {
15
15
  draft: 'Draft',
16
16
  running: 'Running',