@delmaredigital/payload-experiments 0.1.0-rc.6 → 0.2.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 (208) hide show
  1. package/README.md +28 -0
  2. package/dist/admin/LayerUsage.d.ts +1 -1
  3. package/dist/admin/LayerUsage.js +23 -11
  4. package/dist/admin/MetricSourceSelect.js +2 -1
  5. package/dist/admin/TestsNavLink.d.ts +1 -0
  6. package/dist/admin/TestsNavLink.js +30 -0
  7. package/dist/admin/allocation.model.js +1 -1
  8. package/dist/admin/copy/actions.d.ts +0 -2
  9. package/dist/admin/copy/actions.js +0 -2
  10. package/dist/admin/copy/advanced.d.ts +55 -0
  11. package/dist/admin/copy/advanced.js +55 -0
  12. package/dist/admin/copy/armSelect.js +1 -1
  13. package/dist/admin/copy/arms.d.ts +0 -13
  14. package/dist/admin/copy/arms.js +0 -13
  15. package/dist/admin/copy/{who.d.ts → audience.d.ts} +1 -9
  16. package/dist/admin/copy/{who.js → audience.js} +1 -9
  17. package/dist/admin/copy/history.d.ts +0 -4
  18. package/dist/admin/copy/history.js +3 -7
  19. package/dist/admin/copy/metrics.d.ts +2 -2
  20. package/dist/admin/copy/metrics.js +2 -2
  21. package/dist/admin/copy/newTest.d.ts +32 -0
  22. package/dist/admin/copy/newTest.js +32 -0
  23. package/dist/admin/copy/readiness.d.ts +45 -0
  24. package/dist/admin/copy/readiness.js +50 -0
  25. package/dist/admin/copy/recommendation.d.ts +26 -0
  26. package/dist/admin/copy/recommendation.js +25 -0
  27. package/dist/admin/copy/results.d.ts +0 -1
  28. package/dist/admin/copy/results.js +0 -1
  29. package/dist/admin/copy/review.d.ts +0 -17
  30. package/dist/admin/copy/review.js +1 -112
  31. package/dist/admin/copy/setting.d.ts +10 -0
  32. package/dist/admin/copy/setting.js +10 -0
  33. package/dist/admin/copy/surface.d.ts +36 -0
  34. package/dist/admin/copy/surface.js +47 -0
  35. package/dist/admin/copy/testPage.d.ts +88 -0
  36. package/dist/admin/copy/testPage.js +88 -0
  37. package/dist/admin/paths.d.ts +5 -13
  38. package/dist/admin/paths.js +5 -13
  39. package/dist/admin/review/review.model.d.ts +3 -26
  40. package/dist/admin/review/review.model.js +3 -30
  41. package/dist/admin/surface/ResultsRoute.d.ts +7 -0
  42. package/dist/admin/surface/ResultsRoute.js +58 -0
  43. package/dist/admin/surface/SurfaceShell.d.ts +6 -0
  44. package/dist/admin/surface/SurfaceShell.js +35 -0
  45. package/dist/admin/surface/TestsHome.d.ts +4 -0
  46. package/dist/admin/surface/TestsHome.js +212 -0
  47. package/dist/admin/surface/TestsSurface.d.ts +2 -0
  48. package/dist/admin/surface/TestsSurface.js +36 -0
  49. package/dist/admin/surface/contracts.d.ts +48 -0
  50. package/dist/admin/surface/contracts.js +8 -0
  51. package/dist/admin/surface/home.model.d.ts +41 -0
  52. package/dist/admin/surface/home.model.js +59 -0
  53. package/dist/admin/surface/newTest/NewTest.d.ts +7 -0
  54. package/dist/admin/surface/newTest/NewTest.js +355 -0
  55. package/dist/admin/surface/newTest/newTest.css +9 -0
  56. package/dist/admin/surface/pageBlock/pageBlock.model.d.ts +6 -0
  57. package/dist/admin/surface/pageBlock/pageBlock.model.js +8 -0
  58. package/dist/admin/surface/pageBlock/parts.d.ts +2 -0
  59. package/dist/admin/surface/pageBlock/parts.js +64 -0
  60. package/dist/admin/surface/readiness/ActionDialogs.d.ts +15 -0
  61. package/dist/admin/surface/readiness/ActionDialogs.js +274 -0
  62. package/dist/admin/surface/readiness/Banner.d.ts +2 -0
  63. package/dist/admin/surface/readiness/Banner.js +138 -0
  64. package/dist/admin/surface/readiness/SaveStatus.d.ts +4 -0
  65. package/dist/admin/surface/readiness/SaveStatus.js +50 -0
  66. package/dist/admin/surface/readiness/parts.d.ts +2 -0
  67. package/dist/admin/surface/readiness/parts.js +9 -0
  68. package/dist/admin/surface/readiness/readiness.model.d.ts +22 -0
  69. package/dist/admin/surface/readiness/readiness.model.js +107 -0
  70. package/dist/admin/surface/readiness/useTransition.d.ts +10 -0
  71. package/dist/admin/surface/readiness/useTransition.js +34 -0
  72. package/dist/admin/surface/results/ResultsBody.d.ts +7 -0
  73. package/dist/admin/surface/results/ResultsBody.js +262 -0
  74. package/dist/admin/surface/results/recommendation.model.d.ts +12 -0
  75. package/dist/admin/surface/results/recommendation.model.js +59 -0
  76. package/dist/admin/surface/results/useResults.d.ts +56 -0
  77. package/dist/admin/surface/results/useResults.js +183 -0
  78. package/dist/admin/surface/setting/parts.d.ts +3 -0
  79. package/dist/admin/surface/setting/parts.js +263 -0
  80. package/dist/admin/surface/setting/setting.css +29 -0
  81. package/dist/admin/surface/setting/setting.model.d.ts +8 -0
  82. package/dist/admin/surface/setting/setting.model.js +25 -0
  83. package/dist/admin/surface/surface.css +50 -0
  84. package/dist/admin/surface/test/AdvancedSection.d.ts +2 -0
  85. package/dist/admin/surface/test/AdvancedSection.js +153 -0
  86. package/dist/admin/surface/test/GoalSection.d.ts +2 -0
  87. package/dist/admin/surface/test/GoalSection.js +198 -0
  88. package/dist/admin/surface/test/LengthSection.d.ts +2 -0
  89. package/dist/admin/surface/test/LengthSection.js +87 -0
  90. package/dist/admin/surface/test/Section.d.ts +7 -0
  91. package/dist/admin/surface/test/Section.js +27 -0
  92. package/dist/admin/surface/test/TargetingSection.d.ts +2 -0
  93. package/dist/admin/surface/test/TargetingSection.js +164 -0
  94. package/dist/admin/surface/test/TestHeader.d.ts +4 -0
  95. package/dist/admin/surface/test/TestHeader.js +69 -0
  96. package/dist/admin/surface/test/TestPage.d.ts +7 -0
  97. package/dist/admin/surface/test/TestPage.js +84 -0
  98. package/dist/admin/surface/test/VersionsSection.d.ts +4 -0
  99. package/dist/admin/surface/test/VersionsSection.js +208 -0
  100. package/dist/admin/surface/test/fields.d.ts +55 -0
  101. package/dist/admin/surface/test/fields.js +185 -0
  102. package/dist/admin/surface/test/pageParts.d.ts +2 -0
  103. package/dist/admin/surface/test/pageParts.js +39 -0
  104. package/dist/admin/surface/test/testPage.css +23 -0
  105. package/dist/admin/surface/test/testPage.model.d.ts +17 -0
  106. package/dist/admin/surface/test/testPage.model.js +53 -0
  107. package/dist/admin/surface/testDocument.model.d.ts +39 -0
  108. package/dist/admin/surface/testDocument.model.js +61 -0
  109. package/dist/admin/surface/useTestDocument.d.ts +22 -0
  110. package/dist/admin/surface/useTestDocument.js +345 -0
  111. package/dist/admin/targeting/targeting.model.js +2 -2
  112. package/dist/admin/useLayerUsage.d.ts +0 -5
  113. package/dist/admin/useLayerUsage.js +1 -39
  114. package/dist/admin/views/CollectionEditRedirect.d.ts +4 -0
  115. package/dist/admin/views/CollectionEditRedirect.js +14 -0
  116. package/dist/admin/views/CollectionListRedirect.d.ts +2 -0
  117. package/dist/admin/views/CollectionListRedirect.js +8 -0
  118. package/dist/admin/views/ExperimentsRootView.d.ts +2 -0
  119. package/dist/admin/views/ExperimentsRootView.js +175 -0
  120. package/dist/admin/views/access.model.d.ts +7 -0
  121. package/dist/admin/views/access.model.js +5 -0
  122. package/dist/collections/experiments/config.js +18 -125
  123. package/dist/collections/experiments/createDefaults.js +6 -0
  124. package/dist/collections/experiments/flagControlParams.js +8 -14
  125. package/dist/collections/layers.js +45 -13
  126. package/dist/collections/metrics.js +70 -14
  127. package/dist/collections/serialize.js +4 -1
  128. package/dist/db/snapshots.d.ts +2 -1
  129. package/dist/db/snapshots.js +2 -2
  130. package/dist/endpoints/results.js +7 -0
  131. package/dist/endpoints/summary.d.ts +1 -1
  132. package/dist/endpoints/summary.js +37 -1
  133. package/dist/exports/adminClient.d.ts +2 -13
  134. package/dist/exports/adminClient.js +2 -13
  135. package/dist/exports/index.d.ts +1 -1
  136. package/dist/exports/rsc.d.ts +3 -0
  137. package/dist/exports/rsc.js +3 -0
  138. package/dist/options.js +29 -0
  139. package/dist/plugin.js +25 -0
  140. package/dist/puck/editor/CreateExperimentDrawer.d.ts +2 -1
  141. package/dist/puck/editor/CreateExperimentDrawer.js +17 -57
  142. package/dist/puck/editor/ExperimentSlotEditor.js +6 -0
  143. package/dist/puck/editor/copy.d.ts +10 -9
  144. package/dist/puck/editor/copy.js +11 -10
  145. package/dist/puck/editor/createTest.d.ts +7 -0
  146. package/dist/puck/editor/createTest.js +50 -0
  147. package/dist/puck/editor/editor.model.d.ts +28 -1
  148. package/dist/puck/editor/editor.model.js +61 -0
  149. package/dist/results/analyse.js +4 -1
  150. package/dist/results/verdict.d.ts +1 -0
  151. package/dist/results/verdict.js +7 -4
  152. package/dist/transitions/checkStart.d.ts +1 -0
  153. package/dist/transitions/placements.d.ts +5 -1
  154. package/dist/transitions/placements.js +14 -7
  155. package/dist/transitions/preflightData.d.ts +7 -1
  156. package/dist/transitions/preflightData.js +199 -148
  157. package/dist/transitions/start.js +2 -2
  158. package/dist/types/admin.d.ts +1 -0
  159. package/dist/types/contracts.d.ts +17 -0
  160. package/dist/types/endpoints.d.ts +11 -1
  161. package/dist/types/snapshot.d.ts +1 -0
  162. package/dist/types/surface.d.ts +29 -0
  163. package/dist/types/surface.js +1 -0
  164. package/package.json +1 -1
  165. package/dist/access.d.ts +0 -1
  166. package/dist/access.js +0 -2
  167. package/dist/admin/AllocationField.d.ts +0 -2
  168. package/dist/admin/AllocationField.js +0 -80
  169. package/dist/admin/FlagPointSelect.d.ts +0 -2
  170. package/dist/admin/FlagPointSelect.js +0 -54
  171. package/dist/admin/LockedFieldNotice.d.ts +0 -2
  172. package/dist/admin/LockedFieldNotice.js +0 -26
  173. package/dist/admin/PhaseHistory.d.ts +0 -2
  174. package/dist/admin/PhaseHistory.js +0 -76
  175. package/dist/admin/StatusCell.d.ts +0 -3
  176. package/dist/admin/StatusCell.js +0 -19
  177. package/dist/admin/SummaryCell.d.ts +0 -2
  178. package/dist/admin/SummaryCell.js +0 -32
  179. package/dist/admin/actions/AddMetricDialog.d.ts +0 -6
  180. package/dist/admin/actions/AddMetricDialog.js +0 -138
  181. package/dist/admin/actions/AllocateDialog.d.ts +0 -6
  182. package/dist/admin/actions/AllocateDialog.js +0 -81
  183. package/dist/admin/actions/ExperimentActions.d.ts +0 -3
  184. package/dist/admin/actions/ExperimentActions.js +0 -110
  185. package/dist/admin/arms/ArmsField.d.ts +0 -2
  186. package/dist/admin/arms/ArmsField.js +0 -310
  187. package/dist/admin/arms/ParamsEditor.d.ts +0 -16
  188. package/dist/admin/arms/ParamsEditor.js +0 -79
  189. package/dist/admin/copy/tabs.d.ts +0 -7
  190. package/dist/admin/copy/tabs.js +0 -7
  191. package/dist/admin/permissions.model.d.ts +0 -10
  192. package/dist/admin/permissions.model.js +0 -4
  193. package/dist/admin/phaseHistory.model.d.ts +0 -19
  194. package/dist/admin/phaseHistory.model.js +0 -19
  195. package/dist/admin/results/ResultsView.d.ts +0 -3
  196. package/dist/admin/results/ResultsView.js +0 -312
  197. package/dist/admin/review/ReviewPanel.d.ts +0 -2
  198. package/dist/admin/review/ReviewPanel.js +0 -268
  199. package/dist/admin/summaryBatcher.d.ts +0 -8
  200. package/dist/admin/summaryBatcher.js +0 -33
  201. package/dist/admin/targeting/TargetingAttributeField.d.ts +0 -2
  202. package/dist/admin/targeting/TargetingAttributeField.js +0 -31
  203. package/dist/admin/targeting/TargetingOpField.d.ts +0 -2
  204. package/dist/admin/targeting/TargetingOpField.js +0 -45
  205. package/dist/admin/targeting/TargetingValueField.d.ts +0 -2
  206. package/dist/admin/targeting/TargetingValueField.js +0 -142
  207. package/dist/index.d.ts +0 -1
  208. package/dist/index.js +0 -4
@@ -0,0 +1,50 @@
1
+ import { editorCopy } from './copy.js';
2
+ import { isSlugCollision, newTestBody, slugify, uniqueSlug } from './editor.model.js';
3
+ /** Only a server refusal may supply editor-facing error text. */ export class CreateRefused extends Error {
4
+ }
5
+ function refusalMessage(body) {
6
+ const error = body;
7
+ const message = error?.errors?.[0]?.data?.errors?.[0]?.message;
8
+ return typeof message === 'string' && message.trim() ? message : editorCopy.createError;
9
+ }
10
+ /** Shared lookup, complete draft creation and one retry for a racing slug collision. */ export async function createTest(apiRoute, input) {
11
+ const api = apiRoute.replace(/\/$/, '');
12
+ const base = slugify(input.name) || 'test';
13
+ for(let attempt = 0;; attempt++){
14
+ // Suffixes shorten a 64-character base; include those neighbouring slugs in the lookup.
15
+ const lookup = await fetch(`${api}/xp-experiments?where[slug][like]=${encodeURIComponent(base.slice(0, 60))}&depth=0&limit=200&select[slug]=true`, {
16
+ credentials: 'include'
17
+ });
18
+ const found = await lookup.json();
19
+ if (!lookup.ok) throw new CreateRefused(refusalMessage(found));
20
+ if (!Array.isArray(found?.docs)) throw new Error(editorCopy.createError);
21
+ const taken = found.docs.flatMap((doc)=>typeof doc.slug === 'string' ? [
22
+ doc.slug
23
+ ] : []);
24
+ const response = await fetch(`${api}/xp-experiments`, {
25
+ method: 'POST',
26
+ credentials: 'include',
27
+ headers: {
28
+ 'Content-Type': 'application/json'
29
+ },
30
+ body: JSON.stringify(newTestBody({
31
+ ...input,
32
+ slug: uniqueSlug(base, taken)
33
+ }))
34
+ });
35
+ const body = await response.json();
36
+ if (!response.ok) {
37
+ if (isSlugCollision(response.status, body)) {
38
+ if (attempt === 0) continue;
39
+ throw new CreateRefused(editorCopy.nameExists);
40
+ }
41
+ throw new CreateRefused(refusalMessage(body));
42
+ }
43
+ const doc = body?.doc;
44
+ if (typeof doc?.id !== 'string' && typeof doc?.id !== 'number' || typeof doc?.slug !== 'string') throw new Error(editorCopy.createError);
45
+ return {
46
+ id: doc.id,
47
+ slug: doc.slug
48
+ };
49
+ }
50
+ }
@@ -1,4 +1,4 @@
1
- import type { Arm, Status } from '../../types/contracts.js';
1
+ import type { Arm, DocId, Status, UnitType } from '../../types/contracts.js';
2
2
  import type { ArmSlot } from '../../types/puck.js';
3
3
  export type EditorExperiment = {
4
4
  name: string;
@@ -14,3 +14,30 @@ export declare function tabsFor(experiment?: Pick<EditorExperiment, 'arms'> | nu
14
14
  }>;
15
15
  export declare function bannerFor(status?: Status | null, winner?: string): string;
16
16
  export declare function slugify(name: string): string;
17
+ /** Complete draft defaults, shared by the page hand-off and setting creation. */
18
+ export declare function newTestBody(input: {
19
+ kind: 'block' | 'flag';
20
+ name: string;
21
+ slug: string;
22
+ unitType: UnitType;
23
+ flagPoint?: string;
24
+ layer?: DocId;
25
+ hypothesis?: string;
26
+ }): {
27
+ hypothesis?: string | undefined;
28
+ layer?: DocId | undefined;
29
+ flagPoint?: string | undefined;
30
+ name: string;
31
+ slug: string;
32
+ kind: "block" | "flag";
33
+ unitType: UnitType;
34
+ exposurePolicy: string;
35
+ arms: Arm[];
36
+ allocation: number;
37
+ analysis: {
38
+ plannedDurationDays: number;
39
+ };
40
+ };
41
+ export declare function uniqueSlug(base: string, taken: readonly string[]): string;
42
+ /** Payload nests field validation errors under data.errors. Only a unique slug refusal is retryable. */
43
+ export declare function isSlugCollision(status: number, body: unknown): boolean;
@@ -31,3 +31,64 @@ export function bannerFor(status, winner = editorCopy.labels.control) {
31
31
  export function slugify(name) {
32
32
  return name.normalize('NFKD').replace(/[\u0300-\u036f]/g, '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 64).replace(/-+$/g, '');
33
33
  }
34
+ /** Complete draft defaults, shared by the page hand-off and setting creation. */ export function newTestBody(input) {
35
+ const { kind, name, slug, unitType, flagPoint, layer, hypothesis } = input;
36
+ const arms = [
37
+ {
38
+ key: 'control',
39
+ kind: 'control',
40
+ label: 'Original',
41
+ weight: 50,
42
+ ...kind === 'block' ? {
43
+ slot: 'control'
44
+ } : {}
45
+ },
46
+ {
47
+ key: 'variantA',
48
+ kind: 'variant',
49
+ label: 'Variant A',
50
+ weight: 50,
51
+ ...kind === 'block' ? {
52
+ slot: 'variantA'
53
+ } : {}
54
+ }
55
+ ];
56
+ return {
57
+ name,
58
+ slug,
59
+ kind,
60
+ unitType,
61
+ exposurePolicy: kind === 'block' ? 'render' : 'manual',
62
+ arms,
63
+ allocation: 100,
64
+ analysis: {
65
+ plannedDurationDays: 28
66
+ },
67
+ ...flagPoint === undefined ? {} : {
68
+ flagPoint
69
+ },
70
+ ...layer === undefined ? {} : {
71
+ layer
72
+ },
73
+ ...hypothesis === undefined ? {} : {
74
+ hypothesis
75
+ }
76
+ };
77
+ }
78
+ export function uniqueSlug(base, taken) {
79
+ const root = base.slice(0, 64).replace(/-+$/g, '') || 'test';
80
+ const used = new Set(taken);
81
+ if (!used.has(root)) return root;
82
+ for(let index = 2;; index++){
83
+ const suffix = `-${index}`;
84
+ const candidate = root.slice(0, 64 - suffix.length).replace(/-+$/g, '') + suffix;
85
+ if (!used.has(candidate)) return candidate;
86
+ }
87
+ }
88
+ /** Payload nests field validation errors under data.errors. Only a unique slug refusal is retryable. */ export function isSlugCollision(status, body) {
89
+ if (status !== 400 || !body || typeof body !== 'object') return false;
90
+ const error = body;
91
+ const message = typeof error.message === 'string' ? error.message : '';
92
+ if ((error.path === 'slug' || error.field === 'slug' || /\bslug\b/i.test(message)) && /unique|already.*(?:use|exist)|duplicate/i.test(message)) return true;
93
+ return Array.isArray(error.errors) && error.errors.some((item)=>isSlugCollision(status, item)) || isSlugCollision(status, error.data);
94
+ }
@@ -114,6 +114,7 @@ const severity = {
114
114
  };
115
115
  let worst = 'ok';
116
116
  let worstArmLabel = compared[0]?.label ?? '';
117
+ let worstArm = compared[0]?.key ?? '';
117
118
  try {
118
119
  if (!data || data.unavailable !== undefined) throw new Error(data?.unavailable ?? 'Metric data was not supplied');
119
120
  const stats = new Map();
@@ -248,6 +249,7 @@ const severity = {
248
249
  if (severity[status] > severity[worst]) {
249
250
  worst = status;
250
251
  worstArmLabel = arm.label;
252
+ worstArm = arm.key;
251
253
  }
252
254
  }
253
255
  } catch (error) {
@@ -267,7 +269,8 @@ const severity = {
267
269
  verdictGuardrails.push(worst === 'failing' ? {
268
270
  name: definition.name,
269
271
  status: worst,
270
- worstArmLabel
272
+ worstArmLabel,
273
+ worstArm
271
274
  } : {
272
275
  name: definition.name,
273
276
  status: worst
@@ -61,6 +61,7 @@ interface VerdictInputFields {
61
61
  } & ({
62
62
  status: 'failing';
63
63
  worstArmLabel: string;
64
+ worstArm: string;
64
65
  } | {
65
66
  status: 'ok' | 'warning' | 'insufficient';
66
67
  worstArmLabel?: string;
@@ -21,7 +21,7 @@ function chance(value) {
21
21
  }
22
22
  /** Apply spec §8.5 in order to already-computed results; no inference is performed here. */ export function computeVerdict(input) {
23
23
  const rollout = input.phaseKind === 'rollout';
24
- const make = (row, code, finding, action, detail, recommendedArm, line)=>({
24
+ const make = (row, code, finding, action, detail, recommendedArm, line, worstArm)=>({
25
25
  row,
26
26
  code,
27
27
  finding,
@@ -31,6 +31,9 @@ function chance(value) {
31
31
  recommendedArm,
32
32
  ...line === undefined ? {} : {
33
33
  guardrailLine: line
34
+ },
35
+ ...worstArm === undefined ? {} : {
36
+ worstArm
34
37
  }
35
38
  });
36
39
  if (input.primary.unavailable !== undefined) {
@@ -56,7 +59,7 @@ function chance(value) {
56
59
  return make('R3', 'rollout-readout', `Since shipping ${winner}: ${percent(lift)} vs the holdout (CI ${percent(ci[0])} to ${percent(ci[1])}).`, null, 'This describes the shipped version compared with people still seeing the Original.', null, guardrailLine(input));
57
60
  }
58
61
  const failing = input.guardrails.find((item)=>item.status === 'failing');
59
- if (failing) return make(3, 'guardrail-failing', `Stop: ${failing.worstArmLabel} harms ${failing.name}.`, null, 'An outcome that must stay safe has become worse.', input.controlArm);
62
+ if (failing) return make(3, 'guardrail-failing', `Stop: ${failing.worstArmLabel} harms ${failing.name}.`, null, 'An outcome that must stay safe has become worse.', input.controlArm, undefined, failing.worstArm);
60
63
  if (collecting) return make(4, 'collecting', input.status === 'stopped' ? 'Stopped before enough data was collected.' : `Collecting data: ${minimum} of ${input.exposures.minExposuresPerArm} people, ${minimumConverters} of ${input.primary.minSampleSize} conversions.`, null, 'More people or conversions are needed before drawing a conclusion.', null);
61
64
  const variantKeys = new Set(compared.filter((item)=>item.kind === 'variant').map((item)=>item.arm));
62
65
  const variants = input.primary.comparisons.filter((item)=>variantKeys.has(item.arm));
@@ -68,12 +71,12 @@ function chance(value) {
68
71
  ].sort((a, b)=>direction * (b.lift - a.lift))[0];
69
72
  const worst = worse[0];
70
73
  if (variants.length > 0 && worse.length === variants.length) return make(5, 'original-better', 'The Original is better.', 'Decide for the Original.', 'Every tested alternative performed worse.', input.controlArm);
71
- if (worst && !best) return make(6, 'worse-none-better', `${labels(worse)} ${worse.length === 1 ? 'is' : 'are'} worse; no variant is better.`, `Decide for the Original or restart without ${worse.length === 1 ? 'it' : 'them'}.`, 'At least one alternative performed worse and none improved the main outcome.', input.controlArm);
74
+ if (worst && !best) return make(6, 'worse-none-better', `${labels(worse)} ${worse.length === 1 ? 'is' : 'are'} worse; no variant is better.`, `Decide for the Original or restart without ${worse.length === 1 ? 'it' : 'them'}.`, 'At least one alternative performed worse and none improved the main outcome.', input.controlArm, undefined, worst.arm);
72
75
  if (best && !worst) {
73
76
  const finding = better.length >= 2 ? `${better.length === 2 ? 'Both' : `All ${better.length} variants`} beat the Original; not tested against each other — ${best.label} has the larger improvement.` : input.method === 'bayesian' ? `${best.label} is very likely better${chance(best.chanceToWin)}.` : `${best.label} is statistically better: ${percent(best.lift)} (CI ${percent(best.ci[0])} to ${percent(best.ci[1])}).`;
74
77
  return make(7, 'winner', finding, null, 'The main outcome improved compared with the Original.', best.arm, guardrailLine(input));
75
78
  }
76
- if (best && worst) return make(8, 'mixed', `Mixed: ${labels(better)} better, ${labels(worse)} worse.`, 'Review before deciding.', 'The tested alternatives moved the main outcome in different directions.', best.arm);
79
+ if (best && worst) return make(8, 'mixed', `Mixed: ${labels(better)} better, ${labels(worse)} worse.`, 'Review before deciding.', 'The tested alternatives moved the main outcome in different directions.', best.arm, undefined, worst.arm);
77
80
  if (input.status === 'running' && !input.plannedDurationPassed) {
78
81
  if (input.power.feasible === false || input.power.daysRemaining !== null && input.power.daysRemaining > 90) {
79
82
  return make(9, 'keep-running', 'A clear answer would take more than 90 days at this traffic.', 'Consider a bigger change or a different metric.', 'Current traffic is too low for a timely answer.', null);
@@ -1,6 +1,7 @@
1
1
  import type { Arm, DocId, ExperimentKind, ExposurePolicy, FlagPoint, MetricSource, ResolvedOptions, Status, TargetingRule, UnitType } from '../types/contracts.js';
2
2
  import type { ArmIssueCode, PreflightResult } from '../types/endpoints.js';
3
3
  import type { BlockPlacement, FlagPlacement } from './placements.js';
4
+ export type ReadinessCode = ArmIssueCode | 'invalid-status' | 'no-planned-duration' | 'no-primary-metric' | 'no-reach-per-day' | 'page-no-targets' | 'page-user-unit' | 'flag-no-point' | 'flag-params-invalid' | 'flag-point-busy' | 'flag-placement-missing' | 'no-placements' | 'empty-slot' | 'layer-unit-mismatch' | 'layer-full' | 'unknown-metric-source' | 'unit-source-mismatch' | 'undeclared-targeting-key' | 'invalid-targeting-rule';
4
5
  export interface StartExperiment {
5
6
  id: DocId;
6
7
  slug: string;
@@ -13,10 +13,14 @@ export type FlagPlacement = Awaited<ReturnType<NonNullable<FlagPoint['placements
13
13
  /** Each matching ExperimentSlot is checked separately, including multiple slots
14
14
  * in one document: content elsewhere in the document cannot hide an empty slot.
15
15
  */
16
- export declare function scanBlockPlacements(req: PayloadRequest, { slug, puckCollections }: {
16
+ export declare function scanBlockPlacements(req: PayloadRequest, args: {
17
17
  slug: string;
18
18
  puckCollections: NonNullable<ResolvedOptions['puck']>['collections'];
19
19
  }): Promise<BlockPlacement[]>;
20
+ export declare function scanBlockPlacements(req: PayloadRequest, args: {
21
+ slugs: ReadonlySet<string>;
22
+ puckCollections: NonNullable<ResolvedOptions['puck']>['collections'];
23
+ }): Promise<Map<string, BlockPlacement[]>>;
20
24
  export declare function flagPlacements<TUser extends {
21
25
  id: DocId;
22
26
  }>(options: ResolvedOptions<TUser>, experiment: {
@@ -5,10 +5,17 @@ const slots = [
5
5
  'variantB',
6
6
  'variantC'
7
7
  ];
8
- /** Each matching ExperimentSlot is checked separately, including multiple slots
9
- * in one document: content elsewhere in the document cannot hide an empty slot.
10
- */ export async function scanBlockPlacements(req, { slug, puckCollections }) {
11
- const placements = [];
8
+ export async function scanBlockPlacements(req, args) {
9
+ const { puckCollections } = args;
10
+ const slugs = args.slugs ?? new Set([
11
+ args.slug
12
+ ]);
13
+ const placements = new Map([
14
+ ...slugs
15
+ ].map((slug)=>[
16
+ slug,
17
+ []
18
+ ]));
12
19
  for (const { slug: collection, field } of normalisePuckCollections({
13
20
  puck: {
14
21
  enabled: true,
@@ -54,8 +61,8 @@ const slots = [
54
61
  const props = node.props;
55
62
  const ref = props?.experiment;
56
63
  const experiment = typeof ref === 'string' ? ref : ref && typeof ref === 'object' && 'slug' in ref ? ref.slug : null;
57
- if (node.type === 'ExperimentSlot' && props && experiment === slug) {
58
- placements.push({
64
+ if (node.type === 'ExperimentSlot' && props && typeof experiment === 'string' && slugs.has(experiment)) {
65
+ placements.get(experiment).push({
59
66
  collection,
60
67
  id: doc.id,
61
68
  title: String(doc.title ?? doc.name ?? doc.id),
@@ -76,7 +83,7 @@ const slots = [
76
83
  }
77
84
  }
78
85
  }
79
- return placements;
86
+ return args.slugs ? placements : placements.get(args.slug);
80
87
  }
81
88
  export async function flagPlacements(options, experiment, req) {
82
89
  const point = options.flagPoints?.find((point)=>point.id === experiment.flagPoint);
@@ -2,7 +2,7 @@ import type { PayloadRequest } from 'payload';
2
2
  import type { PowerWeeksInput, PowerWeeksResult } from '@delmaredigital/experiments-stats';
3
3
  import type { DocId, ResolvedOptions } from '../types/contracts.js';
4
4
  import type { PreflightResult } from '../types/endpoints.js';
5
- import type { CheckStartInput } from './checkStart.js';
5
+ import type { CheckStartInput, StartExperiment } from './checkStart.js';
6
6
  export interface PreflightDependencies {
7
7
  now?: Date;
8
8
  estimate?: (input: PowerWeeksInput) => PowerWeeksResult | Promise<PowerWeeksResult>;
@@ -13,6 +13,12 @@ export interface PreflightDependencies {
13
13
  export declare function loadStartInputs<TUser extends {
14
14
  id: DocId;
15
15
  }>(req: PayloadRequest, options: ResolvedOptions<TUser>, experimentId: DocId): Promise<CheckStartInput | null>;
16
+ /** Shared, request-scoped reads for summary; a failed row is null in tolerant mode.
17
+ * Preflight/Start use the same builder and retain their throwing read behaviour.
18
+ */
19
+ export declare function loadStartInputsBatch<TUser extends {
20
+ id: DocId;
21
+ }>(req: PayloadRequest, options: ResolvedOptions<TUser>, experiments: StartExperiment[], tolerateErrors?: boolean): Promise<Map<string, CheckStartInput | null>>;
16
22
  export declare function preflightData<TUser extends {
17
23
  id: DocId;
18
24
  }>(req: PayloadRequest, { experimentId, options }: {