@delmaredigital/payload-experiments 0.1.0-rc.7 → 0.2.0-rc.2

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 (207) 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/layers.js +43 -12
  125. package/dist/collections/metrics.js +70 -14
  126. package/dist/collections/serialize.js +4 -1
  127. package/dist/db/snapshots.d.ts +2 -1
  128. package/dist/db/snapshots.js +2 -2
  129. package/dist/endpoints/results.js +7 -0
  130. package/dist/endpoints/summary.d.ts +1 -1
  131. package/dist/endpoints/summary.js +37 -1
  132. package/dist/exports/adminClient.d.ts +2 -13
  133. package/dist/exports/adminClient.js +2 -13
  134. package/dist/exports/adminRsc.d.ts +3 -0
  135. package/dist/exports/adminRsc.js +3 -0
  136. package/dist/exports/index.d.ts +1 -1
  137. package/dist/options.js +29 -0
  138. package/dist/plugin.js +25 -0
  139. package/dist/puck/editor/CreateExperimentDrawer.d.ts +2 -1
  140. package/dist/puck/editor/CreateExperimentDrawer.js +17 -57
  141. package/dist/puck/editor/ExperimentSlotEditor.js +6 -0
  142. package/dist/puck/editor/copy.d.ts +10 -9
  143. package/dist/puck/editor/copy.js +11 -10
  144. package/dist/puck/editor/createTest.d.ts +7 -0
  145. package/dist/puck/editor/createTest.js +50 -0
  146. package/dist/puck/editor/editor.model.d.ts +28 -1
  147. package/dist/puck/editor/editor.model.js +61 -0
  148. package/dist/results/analyse.js +4 -1
  149. package/dist/results/verdict.d.ts +1 -0
  150. package/dist/results/verdict.js +7 -4
  151. package/dist/transitions/checkStart.d.ts +1 -0
  152. package/dist/transitions/placements.d.ts +5 -1
  153. package/dist/transitions/placements.js +14 -7
  154. package/dist/transitions/preflightData.d.ts +7 -1
  155. package/dist/transitions/preflightData.js +199 -148
  156. package/dist/transitions/start.js +2 -2
  157. package/dist/types/admin.d.ts +1 -0
  158. package/dist/types/contracts.d.ts +17 -0
  159. package/dist/types/endpoints.d.ts +11 -1
  160. package/dist/types/snapshot.d.ts +1 -0
  161. package/dist/types/surface.d.ts +29 -0
  162. package/dist/types/surface.js +1 -0
  163. package/package.json +7 -1
  164. package/dist/access.d.ts +0 -1
  165. package/dist/access.js +0 -2
  166. package/dist/admin/AllocationField.d.ts +0 -2
  167. package/dist/admin/AllocationField.js +0 -80
  168. package/dist/admin/FlagPointSelect.d.ts +0 -2
  169. package/dist/admin/FlagPointSelect.js +0 -54
  170. package/dist/admin/LockedFieldNotice.d.ts +0 -2
  171. package/dist/admin/LockedFieldNotice.js +0 -26
  172. package/dist/admin/PhaseHistory.d.ts +0 -2
  173. package/dist/admin/PhaseHistory.js +0 -76
  174. package/dist/admin/StatusCell.d.ts +0 -3
  175. package/dist/admin/StatusCell.js +0 -19
  176. package/dist/admin/SummaryCell.d.ts +0 -2
  177. package/dist/admin/SummaryCell.js +0 -32
  178. package/dist/admin/actions/AddMetricDialog.d.ts +0 -6
  179. package/dist/admin/actions/AddMetricDialog.js +0 -138
  180. package/dist/admin/actions/AllocateDialog.d.ts +0 -6
  181. package/dist/admin/actions/AllocateDialog.js +0 -81
  182. package/dist/admin/actions/ExperimentActions.d.ts +0 -3
  183. package/dist/admin/actions/ExperimentActions.js +0 -110
  184. package/dist/admin/arms/ArmsField.d.ts +0 -2
  185. package/dist/admin/arms/ArmsField.js +0 -310
  186. package/dist/admin/arms/ParamsEditor.d.ts +0 -16
  187. package/dist/admin/arms/ParamsEditor.js +0 -79
  188. package/dist/admin/copy/tabs.d.ts +0 -7
  189. package/dist/admin/copy/tabs.js +0 -7
  190. package/dist/admin/permissions.model.d.ts +0 -10
  191. package/dist/admin/permissions.model.js +0 -4
  192. package/dist/admin/phaseHistory.model.d.ts +0 -19
  193. package/dist/admin/phaseHistory.model.js +0 -19
  194. package/dist/admin/results/ResultsView.d.ts +0 -3
  195. package/dist/admin/results/ResultsView.js +0 -312
  196. package/dist/admin/review/ReviewPanel.d.ts +0 -2
  197. package/dist/admin/review/ReviewPanel.js +0 -268
  198. package/dist/admin/summaryBatcher.d.ts +0 -8
  199. package/dist/admin/summaryBatcher.js +0 -33
  200. package/dist/admin/targeting/TargetingAttributeField.d.ts +0 -2
  201. package/dist/admin/targeting/TargetingAttributeField.js +0 -31
  202. package/dist/admin/targeting/TargetingOpField.d.ts +0 -2
  203. package/dist/admin/targeting/TargetingOpField.js +0 -45
  204. package/dist/admin/targeting/TargetingValueField.d.ts +0 -2
  205. package/dist/admin/targeting/TargetingValueField.js +0 -142
  206. package/dist/index.d.ts +0 -1
  207. package/dist/index.js +0 -4
@@ -0,0 +1,7 @@
1
+ import '../../styles.js';
2
+ import type { DocId } from '../../../types/contracts.js';
3
+ import type { SurfaceProps } from '../contracts.js';
4
+ export declare function ResultsBody({ id, surface }: {
5
+ id: DocId;
6
+ surface: SurfaceProps;
7
+ }): import("react").JSX.Element;
@@ -0,0 +1,262 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import '../../styles.js';
4
+ import { useEffect, useRef, useState } from 'react';
5
+ import { Button } from '@payloadcms/ui';
6
+ import { RESULTS_COPY as C } from '../../copy/results.js';
7
+ import { RESULTS_SURFACE_COPY } from '../../copy/recommendation.js';
8
+ import { ACTIONS_COPY } from '../../copy/actions.js';
9
+ import { API_MESSAGES } from '../../copy/common.js';
10
+ import { armLabel } from '../../labels.js';
11
+ import { PreviewLinks } from '../../PreviewLinks.js';
12
+ import { VerdictCard } from '../../results/VerdictCard.js';
13
+ import { ArmTable } from '../../results/ArmTable.js';
14
+ import { DataQualityPanel } from '../../results/DataQualityPanel.js';
15
+ import { CiChart } from '../../results/CiChart.js';
16
+ import { armRows, collectingLine, dataQuality, defaultRound, guardrailChips, powerLine, rolloutLine, roundOptions, verdictCard } from '../../results/results.model.js';
17
+ import { recommendation } from './recommendation.model.js';
18
+ import { useResults } from './useResults.js';
19
+ import { DecideDialog } from '../../actions/DecideDialog.js';
20
+ export function ResultsBody({ id, surface }) {
21
+ // A different test owns an independent selection and action lifecycle.
22
+ return /*#__PURE__*/ _jsx(ResultsContent, {
23
+ id: id,
24
+ surface: surface
25
+ }, `${surface.apiRoute}/${id}`);
26
+ }
27
+ function ResultsContent({ id, surface }) {
28
+ const results = useResults(id, surface);
29
+ const { api, doc, round, phases, state, data } = results;
30
+ const manage = surface.canManage && surface.canReadResults;
31
+ const [placements, setPlacements] = useState(null);
32
+ const [previewError, setPreviewError] = useState(null);
33
+ const [previewRefresh, setPreviewRefresh] = useState(0);
34
+ const [busy, setBusy] = useState(false);
35
+ const inFlight = useRef(false);
36
+ const mounted = useRef(true);
37
+ const [actionError, setActionError] = useState(null);
38
+ const [deciding, setDeciding] = useState(false);
39
+ useEffect(()=>{
40
+ mounted.current = true;
41
+ return ()=>{
42
+ mounted.current = false;
43
+ };
44
+ }, []);
45
+ useEffect(()=>{
46
+ let active = true;
47
+ setPlacements(null);
48
+ setPreviewError(null);
49
+ if (manage) void api.preflight(id).then((result)=>{
50
+ if (!active) return;
51
+ if (result.ok && result.data) setPlacements(result.data.placements);
52
+ else setPreviewError(result.ok ? API_MESSAGES.unreadable : result.message);
53
+ });
54
+ return ()=>{
55
+ active = false;
56
+ };
57
+ }, [
58
+ api,
59
+ id,
60
+ manage,
61
+ previewRefresh
62
+ ]);
63
+ async function recompute() {
64
+ if (!manage || inFlight.current) return;
65
+ inFlight.current = true;
66
+ setBusy(true);
67
+ setActionError(null);
68
+ try {
69
+ const result = await api.recompute(id);
70
+ if (!mounted.current) return;
71
+ if (result.ok) results.reload();
72
+ else setActionError(result.message);
73
+ } finally{
74
+ inFlight.current = false;
75
+ if (mounted.current) setBusy(false);
76
+ }
77
+ }
78
+ const phase = data?.phases.find((phase)=>phase.phase === round);
79
+ const snapshot = data?.snapshot;
80
+ const newestRound = defaultRound({
81
+ phases,
82
+ status: 'running'
83
+ });
84
+ const latestPhase = phases.find((phase)=>phase.phase === newestRound);
85
+ const decision = data?.decision ?? doc?.decision;
86
+ const status = doc?.status ?? (data?.decision ? 'decided' : latestPhase?.phaseKind === 'test' && latestPhase.endedAt === null ? 'running' : 'stopped');
87
+ // ADM-06b: an older round never inherits the live test's running status.
88
+ const lineStatus = status === 'running' && round === newestRound ? 'running' : 'stopped';
89
+ const isDecidedTestRound = status === 'decided' && phase?.phaseKind === 'test' && decision?.phase === round;
90
+ const winner = phase?.arms.find((arm)=>arm.key === decision?.winnerArm)?.label ?? armLabel(decision?.winnerArm ?? 'control');
91
+ const card = snapshot ? verdictCard({
92
+ snapshot,
93
+ decisionSnapshot: data?.decisionSnapshot,
94
+ isDecidedTestRound,
95
+ winnerLabel: winner
96
+ }) : null;
97
+ const context = {
98
+ phase,
99
+ inLayer: doc?.layer != null,
100
+ layerSlices: doc?.layerSlices,
101
+ allocation: doc?.allocation
102
+ };
103
+ const primary = phase?.metrics.find((metric)=>metric.role === 'primary');
104
+ // The endpoint decides the current round; historical rounds are read-only.
105
+ const canDecide = manage && (status === 'running' || status === 'stopped') && phase?.phaseKind === 'test' && round === newestRound && state === 'ready' && Boolean(snapshot);
106
+ return /*#__PURE__*/ _jsxs("div", {
107
+ className: "xp-results",
108
+ style: {
109
+ color: 'var(--theme-text)'
110
+ },
111
+ children: [
112
+ phases.length > 0 ? /*#__PURE__*/ _jsxs("label", {
113
+ children: [
114
+ C.round,
115
+ /*#__PURE__*/ _jsx("select", {
116
+ disabled: deciding,
117
+ value: round ?? '',
118
+ onChange: (event)=>results.setRound(Number(event.target.value)),
119
+ children: roundOptions(phases).map((option)=>/*#__PURE__*/ _jsx("option", {
120
+ value: option.value,
121
+ children: option.label
122
+ }, option.value))
123
+ })
124
+ ]
125
+ }) : null,
126
+ state === 'loading' ? /*#__PURE__*/ _jsx("p", {
127
+ role: "status",
128
+ children: data ? C.refreshing : C.loading
129
+ }) : state === 'forbidden' ? /*#__PURE__*/ _jsx("p", {
130
+ role: "alert",
131
+ children: C.forbidden
132
+ }) : state === 'missing' ? /*#__PURE__*/ _jsx("p", {
133
+ role: "alert",
134
+ children: RESULTS_SURFACE_COPY.missing
135
+ }) : state === 'error' ? /*#__PURE__*/ _jsxs(_Fragment, {
136
+ children: [
137
+ /*#__PURE__*/ _jsx("p", {
138
+ role: "alert",
139
+ children: results.error
140
+ }),
141
+ /*#__PURE__*/ _jsx(Button, {
142
+ onClick: results.reload,
143
+ children: C.retry
144
+ })
145
+ ]
146
+ }) : null,
147
+ phase && snapshot ? /*#__PURE__*/ _jsxs(_Fragment, {
148
+ children: [
149
+ /*#__PURE__*/ _jsxs("div", {
150
+ style: {
151
+ background: 'var(--theme-elevation-50)',
152
+ paddingTop: '1rem'
153
+ },
154
+ children: [
155
+ /*#__PURE__*/ _jsx("p", {
156
+ className: "xp-recommendation",
157
+ style: {
158
+ margin: '0 1rem'
159
+ },
160
+ children: recommendation({
161
+ snapshot,
162
+ phase,
163
+ status: lineStatus,
164
+ plannedDurationDays: round === newestRound ? doc?.analysis?.plannedDurationDays : undefined
165
+ })
166
+ }),
167
+ /*#__PURE__*/ _jsx(VerdictCard, {
168
+ model: card,
169
+ guardrails: guardrailChips(snapshot),
170
+ power: powerLine({
171
+ snapshot,
172
+ phaseKind: phase.phaseKind,
173
+ status: lineStatus
174
+ }),
175
+ collecting: collectingLine({
176
+ snapshot,
177
+ phaseKind: phase.phaseKind,
178
+ status: lineStatus
179
+ }, context),
180
+ sinceShipping: rolloutLine(snapshot, phase, phases, decision ?? undefined)
181
+ })
182
+ ]
183
+ }),
184
+ /*#__PURE__*/ _jsx(DataQualityPanel, {
185
+ items: dataQuality(snapshot, context)
186
+ }),
187
+ [
188
+ 'primary',
189
+ 'secondary',
190
+ 'guardrail'
191
+ ].map((role)=>{
192
+ const metrics = phase.metrics.filter((metric)=>metric.role === role);
193
+ return metrics.length ? /*#__PURE__*/ _jsxs("section", {
194
+ children: [
195
+ /*#__PURE__*/ _jsx("h2", {
196
+ children: C[role]
197
+ }),
198
+ metrics.map((metric)=>/*#__PURE__*/ _jsx(ArmTable, {
199
+ caption: metric.name,
200
+ method: snapshot.method,
201
+ metricKind: metric.kind,
202
+ rows: armRows({
203
+ snapshot,
204
+ phase,
205
+ metricId: metric.metricId
206
+ })
207
+ }, metric.metricId))
208
+ ]
209
+ }, role) : null;
210
+ }),
211
+ primary ? /*#__PURE__*/ _jsx(CiChart, {
212
+ series: data?.series,
213
+ metricId: primary.metricId,
214
+ metricName: primary.name,
215
+ phase: phase,
216
+ contentChanges: snapshot.contentChanges
217
+ }) : null
218
+ ]
219
+ }) : state === 'empty' || state === 'ready' ? /*#__PURE__*/ _jsx("p", {
220
+ children: C.empty
221
+ }) : null,
222
+ manage && placements ? /*#__PURE__*/ _jsx(PreviewLinks, {
223
+ experimentId: id,
224
+ placements: placements,
225
+ arms: doc?.arms ?? []
226
+ }) : null,
227
+ manage && previewError ? /*#__PURE__*/ _jsxs(_Fragment, {
228
+ children: [
229
+ /*#__PURE__*/ _jsx("p", {
230
+ role: "alert",
231
+ children: previewError
232
+ }),
233
+ /*#__PURE__*/ _jsx(Button, {
234
+ onClick: ()=>setPreviewRefresh((value)=>value + 1),
235
+ children: C.retryPreview
236
+ })
237
+ ]
238
+ }) : null,
239
+ manage ? /*#__PURE__*/ _jsx(Button, {
240
+ disabled: busy || deciding,
241
+ onClick: ()=>{
242
+ void recompute();
243
+ },
244
+ children: busy ? C.recomputing : C.recompute
245
+ }) : null,
246
+ canDecide ? /*#__PURE__*/ _jsx(Button, {
247
+ disabled: busy || deciding,
248
+ onClick: ()=>setDeciding(true),
249
+ children: ACTIONS_COPY.decide
250
+ }) : null,
251
+ deciding && manage && phase ? /*#__PURE__*/ _jsx(DecideDialog, {
252
+ id: id,
253
+ arms: phase.arms,
254
+ onClose: ()=>setDeciding(false)
255
+ }) : null,
256
+ actionError ? /*#__PURE__*/ _jsx("p", {
257
+ role: "alert",
258
+ children: actionError
259
+ }) : null
260
+ ]
261
+ });
262
+ }
@@ -0,0 +1,12 @@
1
+ import type { PhaseSummary } from '../../../types/endpoints.js';
2
+ import type { Status } from '../../../types/contracts.js';
3
+ import type { Snapshot, VerdictCode } from '../../../types/snapshot.js';
4
+ export interface RecommendationContext {
5
+ snapshot: Snapshot;
6
+ phase?: PhaseSummary;
7
+ plannedDurationDays?: number | null;
8
+ status?: Status;
9
+ }
10
+ /** Select stored findings and format them; never run inference in the admin. */
11
+ export declare const RECOMMENDATION: Record<VerdictCode, (ctx: RecommendationContext) => string>;
12
+ export declare const recommendation: (ctx: RecommendationContext) => string;
@@ -0,0 +1,59 @@
1
+ import { armLabel } from '../../labels.js';
2
+ import { formatCount, formatPercent } from '../../results/format.js';
3
+ import { RECOMMENDATION_COPY as C, collectingRecommendation, guardrailRecommendation, mixedRecommendation, rolloutCollectingRecommendation, rolloutReadoutRecommendation, runningRecommendation, winnerRecommendation, worseRecommendation, storedGuardrailFinding } from '../../copy/recommendation.js';
4
+ function label(ctx, key, fallback = C.group) {
5
+ const stored = ctx.phase?.arms.find((arm)=>arm.key === key)?.label.trim();
6
+ return stored || (key && [
7
+ 'control',
8
+ 'variantA',
9
+ 'variantB',
10
+ 'variantC',
11
+ 'holdout'
12
+ ].includes(key) ? armLabel(key) : fallback);
13
+ }
14
+ const primary = ({ snapshot })=>snapshot.metrics.find((metric)=>metric.role === 'primary');
15
+ const worse = (ctx)=>primary(ctx)?.comparisons.find((comparison)=>comparison.significant === 'worse' && ctx.phase?.arms.some((arm)=>arm.key === comparison.arm && arm.kind === 'variant'))?.arm;
16
+ const winner = (ctx)=>label(ctx, ctx.snapshot.winnerArm ?? ctx.snapshot.verdict.recommendedArm, C.winner);
17
+ /** Select stored findings and format them; never run inference in the admin. */ export const RECOMMENDATION = {
18
+ unavailable: ()=>C.unavailable,
19
+ 'data-problem': ()=>C.dataProblem,
20
+ 'guardrail-failing': (ctx)=>{
21
+ const guardrail = ctx.snapshot.guardrails.find((guardrail)=>guardrail.status === 'failing');
22
+ const metric = ctx.snapshot.metrics.find((metric)=>metric.metricId === guardrail?.metricId);
23
+ // Guardrail safety uses one-sided tests, distinct from comparison significance.
24
+ // Select the group the server named; do not re-infer harm from the table.
25
+ const group = ctx.phase?.arms.find((arm)=>ctx.snapshot.verdict.finding === storedGuardrailFinding(arm.label.trim() || armLabel(arm.key), metric?.name ?? C.guardrail));
26
+ return guardrailRecommendation(label(ctx, ctx.snapshot.verdict.worstArm ?? group?.key), metric?.name ?? C.guardrail);
27
+ },
28
+ collecting: (ctx)=>{
29
+ if (ctx.status === 'stopped') return C.stoppedCollecting;
30
+ const needed = ctx.snapshot.power.exposuresNeededPerArm;
31
+ const groups = ctx.phase?.arms.filter((arm)=>arm.kind !== 'holdout').length ?? ctx.snapshot.k + 1;
32
+ return needed === null ? C.collecting : collectingRecommendation(formatCount(Math.round(needed * groups)));
33
+ },
34
+ 'original-better': ()=>C.originalBetter,
35
+ 'worse-none-better': (ctx)=>worseRecommendation(label(ctx, worse(ctx))),
36
+ winner: (ctx)=>winnerRecommendation(label(ctx, ctx.snapshot.verdict.recommendedArm)),
37
+ mixed: (ctx)=>mixedRecommendation(label(ctx, ctx.snapshot.verdict.recommendedArm), label(ctx, worse(ctx))),
38
+ 'keep-running': (ctx)=>{
39
+ // Same condition as the server's long-duration branch (src/results/verdict.ts).
40
+ const power = ctx.snapshot.power;
41
+ if (!power.feasible || power.daysRemaining !== null && power.daysRemaining > 90) return C.longDuration;
42
+ const days = ctx.plannedDurationDays;
43
+ const start = Date.parse(ctx.phase?.startedAt ?? '');
44
+ const end = days != null && Number.isFinite(days) && days > 0 ? start + days * 86_400_000 : NaN;
45
+ return Number.isFinite(end) ? runningRecommendation(new Intl.DateTimeFormat('en-US', {
46
+ month: 'short',
47
+ day: 'numeric',
48
+ year: 'numeric',
49
+ timeZone: 'UTC'
50
+ }).format(end)) : C.keepRunning;
51
+ },
52
+ 'no-difference': ()=>C.noDifference,
53
+ 'rollout-collecting': (ctx)=>rolloutCollectingRecommendation(winner(ctx)),
54
+ 'rollout-readout': (ctx)=>rolloutReadoutRecommendation(winner(ctx), formatPercent(primary(ctx)?.comparisons.find((comparison)=>comparison.arm === (ctx.snapshot.winnerArm ?? ctx.snapshot.verdict.recommendedArm))?.lift, true))
55
+ };
56
+ export const recommendation = (ctx)=>{
57
+ const sentence = RECOMMENDATION[ctx.snapshot.verdict.code];
58
+ return sentence(ctx);
59
+ };
@@ -0,0 +1,56 @@
1
+ import type { DocId, Status } from '../../../types/contracts.js';
2
+ import type { PhaseSummary, ResultsResponse } from '../../../types/endpoints.js';
3
+ import { type ApiResult } from '../../api.js';
4
+ export interface ResultsDocument {
5
+ status: Status;
6
+ arms: Array<{
7
+ key: string;
8
+ kind?: string;
9
+ label?: string;
10
+ }>;
11
+ decision?: {
12
+ phase: number;
13
+ winnerArm?: string;
14
+ } | null;
15
+ analysis?: {
16
+ plannedDurationDays?: number | null;
17
+ } | null;
18
+ layer?: unknown;
19
+ layerSlices?: number[];
20
+ allocation?: number;
21
+ }
22
+ export type ResultsLoadState = 'loading' | 'ready' | 'empty' | 'forbidden' | 'missing' | 'error';
23
+ export declare function useResults(id: DocId, { apiRoute, canReadResults, canManage }: {
24
+ apiRoute: string;
25
+ canReadResults: boolean;
26
+ canManage?: boolean;
27
+ }): {
28
+ api: {
29
+ start: (id: DocId) => Promise<ApiResult<import("../../../types/endpoints.js").TransitionSuccess>>;
30
+ allocate: (id: DocId, body: import("../../../types/endpoints.js").AllocateBody) => Promise<ApiResult<import("../../../types/endpoints.js").TransitionSuccess>>;
31
+ addMetric: (id: DocId, body: import("../../../types/endpoints.js").AddMetricBody) => Promise<ApiResult<import("../../../types/endpoints.js").TransitionSuccess>>;
32
+ stop: (id: DocId) => Promise<ApiResult<import("../../../types/endpoints.js").TransitionSuccess>>;
33
+ decide: (id: DocId, body: import("../../../types/endpoints.js").DecideBody) => Promise<ApiResult<import("../../../types/endpoints.js").TransitionSuccess>>;
34
+ releaseHoldout: (id: DocId) => Promise<ApiResult<import("../../../types/endpoints.js").TransitionSuccess>>;
35
+ makeContent: (id: DocId) => Promise<ApiResult<import("../../../types/endpoints.js").MakeContentResult>>;
36
+ archive: (id: DocId, body: import("../../../types/endpoints.js").ArchiveBody) => Promise<ApiResult<import("../../../types/endpoints.js").TransitionSuccess>>;
37
+ preflight: (id: DocId) => Promise<ApiResult<import("../../../types/endpoints.js").PreflightResult>>;
38
+ previewToken: (id: DocId, arm: string) => Promise<ApiResult<import("../../../types/endpoints.js").PreviewTokenResult>>;
39
+ results: (id: DocId, options?: {
40
+ phase?: number;
41
+ series?: boolean;
42
+ }) => Promise<ApiResult<ResultsResponse>>;
43
+ recompute: (id: DocId) => Promise<ApiResult<{
44
+ snapshotId: string;
45
+ }>>;
46
+ summary: (ids: readonly DocId[]) => Promise<ApiResult<import("../../../types/endpoints.js").SummaryResponse>>;
47
+ };
48
+ state: ResultsLoadState;
49
+ data: ResultsResponse | undefined;
50
+ phases: PhaseSummary[];
51
+ round: number | null;
52
+ doc: ResultsDocument | undefined;
53
+ error: string | undefined;
54
+ setRound: (next: number) => void;
55
+ reload: () => void;
56
+ };
@@ -0,0 +1,183 @@
1
+ 'use client';
2
+ import { useEffect, useMemo, useState } from 'react';
3
+ import { createApi } from '../../api.js';
4
+ import { API_MESSAGES, refusalMessage } from '../../copy/common.js';
5
+ import { defaultRound } from '../../results/results.model.js';
6
+ // The shared API has no collection-document read.
7
+ async function read(url, signal) {
8
+ try {
9
+ const response = await fetch(url, {
10
+ method: 'GET',
11
+ credentials: 'include',
12
+ signal
13
+ });
14
+ const data = await response.json().catch(()=>undefined);
15
+ if (response.ok) return {
16
+ ok: true,
17
+ data
18
+ };
19
+ const envelope = data;
20
+ return {
21
+ ok: false,
22
+ status: response.status,
23
+ message: refusalMessage(undefined, response.status, envelope?.errors?.[0]?.message),
24
+ retryable: response.status >= 500
25
+ };
26
+ } catch {
27
+ return {
28
+ ok: false,
29
+ status: 0,
30
+ message: API_MESSAGES.network,
31
+ retryable: true
32
+ };
33
+ }
34
+ }
35
+ export function useResults(id, { apiRoute, canReadResults, canManage = true }) {
36
+ const base = apiRoute.replace(/\/$/, '');
37
+ const key = JSON.stringify([
38
+ base,
39
+ String(id),
40
+ canReadResults,
41
+ canManage
42
+ ]);
43
+ const api = useMemo(()=>createApi(base), [
44
+ base
45
+ ]);
46
+ const [selection, setSelection] = useState({
47
+ key,
48
+ round: null
49
+ });
50
+ const round = selection.key === key ? selection.round : null;
51
+ const [refresh, setRefresh] = useState(0);
52
+ const [view, setView] = useState({
53
+ key,
54
+ round: null,
55
+ state: 'loading'
56
+ });
57
+ useEffect(()=>{
58
+ const controller = new AbortController();
59
+ const signal = controller.signal;
60
+ if (!canReadResults) {
61
+ setView({
62
+ key,
63
+ round,
64
+ state: 'forbidden'
65
+ });
66
+ return ()=>controller.abort();
67
+ }
68
+ setView((previous)=>({
69
+ key,
70
+ round,
71
+ state: 'loading',
72
+ ...previous.key === key ? {
73
+ doc: previous.doc,
74
+ phases: previous.phases,
75
+ ...previous.round === round ? {
76
+ data: previous.data
77
+ } : {}
78
+ } : {}
79
+ }));
80
+ const encoded = encodeURIComponent(id);
81
+ void Promise.all([
82
+ canManage ? read(`${base}/xp-experiments/${encoded}?depth=0`, signal) : Promise.resolve({
83
+ ok: true,
84
+ data: undefined
85
+ }),
86
+ api.results(id, {
87
+ phase: round ?? undefined,
88
+ series: true
89
+ })
90
+ ]).then(([document, result])=>{
91
+ if (signal.aborted) return;
92
+ const failure = !document.ok ? document : !result.ok ? result : undefined;
93
+ if (failure || !document.ok || !result.ok || canManage && !document.data || !result.data) {
94
+ const state = failure?.status === 403 ? 'forbidden' : failure?.status === 404 ? 'missing' : 'error';
95
+ setView((previous)=>({
96
+ key,
97
+ round,
98
+ state,
99
+ phases: previous.key === key ? previous.phases : undefined,
100
+ error: failure?.message ?? API_MESSAGES.unreadable,
101
+ ...state === 'error' && previous.key === key && previous.round === round ? {
102
+ data: previous.data,
103
+ doc: previous.doc
104
+ } : {}
105
+ }));
106
+ return;
107
+ }
108
+ if (round === null) {
109
+ // Readers cannot GET the manager-only experiment collection. The results
110
+ // endpoint already selects the decision round (or latest test round).
111
+ const decision = result.data.decision ?? document.data?.decision;
112
+ const initial = defaultRound({
113
+ phases: result.data.phases,
114
+ decision,
115
+ status: result.data.decision ? 'decided' : document.data?.status ?? 'running'
116
+ });
117
+ if (initial !== null && initial !== result.data.snapshot?.phase) {
118
+ setView({
119
+ key,
120
+ round: null,
121
+ state: 'loading',
122
+ doc: document.data,
123
+ phases: result.data.phases,
124
+ data: {
125
+ ...result.data,
126
+ snapshot: null
127
+ }
128
+ });
129
+ setSelection({
130
+ key,
131
+ round: initial
132
+ });
133
+ return;
134
+ }
135
+ }
136
+ setView({
137
+ key,
138
+ round: round ?? result.data.snapshot?.phase ?? null,
139
+ state: result.data.snapshot ? 'ready' : 'empty',
140
+ doc: document.data,
141
+ phases: result.data.phases,
142
+ data: result.data
143
+ });
144
+ });
145
+ return ()=>controller.abort();
146
+ }, [
147
+ api,
148
+ base,
149
+ id,
150
+ key,
151
+ canReadResults,
152
+ canManage,
153
+ round,
154
+ refresh
155
+ ]);
156
+ const current = view.key === key;
157
+ const matching = current && (round === null || view.round === round);
158
+ const selectedRound = round ?? (current ? view.round : null);
159
+ const state = !canReadResults ? 'forbidden' : matching ? view.state : 'loading';
160
+ const data = matching && canReadResults ? view.data : undefined;
161
+ // Preserve options while another round loads; its snapshot is never displayed.
162
+ const phases = current && canReadResults ? view.phases ?? [] : [];
163
+ return {
164
+ api,
165
+ state,
166
+ data,
167
+ phases,
168
+ round: selectedRound,
169
+ doc: current && canReadResults ? view.doc : undefined,
170
+ error: matching ? view.error : undefined,
171
+ setRound: (next)=>setSelection({
172
+ key,
173
+ round: next
174
+ }),
175
+ reload: ()=>{
176
+ setSelection({
177
+ key,
178
+ round: selectedRound
179
+ });
180
+ setRefresh((value)=>value + 1);
181
+ }
182
+ };
183
+ }
@@ -0,0 +1,3 @@
1
+ import type { KindParts } from '../contracts.js';
2
+ import './setting.css';
3
+ export declare const settingParts: KindParts;