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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (291) hide show
  1. package/README.md +129 -11
  2. package/dist/admin/PreviewLinks.d.ts +2 -1
  3. package/dist/admin/PreviewLinks.js +24 -16
  4. package/dist/admin/actions/ConfirmDialog.d.ts +7 -4
  5. package/dist/admin/actions/ConfirmDialog.js +37 -82
  6. package/dist/admin/actions/DecideDialog.d.ts +3 -1
  7. package/dist/admin/actions/DecideDialog.js +30 -13
  8. package/dist/admin/actions/MakeContentResult.d.ts +13 -2
  9. package/dist/admin/actions/MakeContentResult.js +115 -16
  10. package/dist/admin/actions/actions.model.d.ts +2 -4
  11. package/dist/admin/actions/actions.model.js +2 -3
  12. package/dist/admin/allocation.model.js +3 -5
  13. package/dist/admin/api.d.ts +10 -4
  14. package/dist/admin/api.js +11 -7
  15. package/dist/admin/copy/actions.d.ts +39 -3
  16. package/dist/admin/copy/actions.js +40 -4
  17. package/dist/admin/copy/armSelect.js +5 -5
  18. package/dist/admin/copy/arms.d.ts +1 -7
  19. package/dist/admin/copy/arms.js +1 -7
  20. package/dist/admin/copy/audience.d.ts +2 -1
  21. package/dist/admin/copy/audience.js +3 -2
  22. package/dist/admin/copy/common.d.ts +13 -6
  23. package/dist/admin/copy/common.js +14 -8
  24. package/dist/admin/copy/goals.d.ts +21 -6
  25. package/dist/admin/copy/goals.js +27 -6
  26. package/dist/admin/copy/readiness.d.ts +19 -0
  27. package/dist/admin/copy/readiness.js +19 -0
  28. package/dist/admin/copy/recommendation.d.ts +0 -3
  29. package/dist/admin/copy/recommendation.js +0 -3
  30. package/dist/admin/copy/results.d.ts +41 -2
  31. package/dist/admin/copy/results.js +42 -3
  32. package/dist/admin/copy/review.d.ts +1 -1
  33. package/dist/admin/copy/review.js +1 -1
  34. package/dist/admin/copy/setting.d.ts +2 -0
  35. package/dist/admin/copy/setting.js +2 -0
  36. package/dist/admin/copy/surface.d.ts +7 -4
  37. package/dist/admin/copy/surface.js +9 -4
  38. package/dist/admin/copy/testPage.d.ts +12 -0
  39. package/dist/admin/copy/testPage.js +12 -0
  40. package/dist/admin/format/date.d.ts +2 -0
  41. package/dist/admin/format/date.js +10 -0
  42. package/dist/admin/labels.d.ts +2 -7
  43. package/dist/admin/labels.js +2 -7
  44. package/dist/admin/previewLinks.model.d.ts +2 -2
  45. package/dist/admin/previewLinks.model.js +5 -7
  46. package/dist/admin/results/CiChart.js +11 -21
  47. package/dist/admin/results/DataQualityPanel.d.ts +4 -2
  48. package/dist/admin/results/DataQualityPanel.js +30 -17
  49. package/dist/admin/results/DataTable.d.ts +16 -0
  50. package/dist/admin/results/DataTable.js +152 -0
  51. package/dist/admin/results/StatTile.d.ts +7 -0
  52. package/dist/admin/results/StatTile.js +22 -0
  53. package/dist/admin/results/VerdictHero.d.ts +13 -0
  54. package/dist/admin/results/{VerdictCard.js → VerdictHero.js} +15 -12
  55. package/dist/admin/results/releasePages.model.d.ts +17 -0
  56. package/dist/admin/results/releasePages.model.js +10 -0
  57. package/dist/admin/results/results.css +64 -0
  58. package/dist/admin/results/results.model.d.ts +6 -2
  59. package/dist/admin/results/results.model.js +12 -5
  60. package/dist/admin/surface/ResultsRoute.d.ts +1 -1
  61. package/dist/admin/surface/ResultsRoute.js +60 -31
  62. package/dist/admin/surface/SurfaceShell.d.ts +11 -2
  63. package/dist/admin/surface/SurfaceShell.js +49 -14
  64. package/dist/admin/surface/TestsHome.d.ts +6 -1
  65. package/dist/admin/surface/TestsHome.js +183 -112
  66. package/dist/admin/surface/TestsSurface.js +56 -16
  67. package/dist/admin/surface/contracts.d.ts +1 -0
  68. package/dist/admin/surface/goals/GoalForm.js +341 -308
  69. package/dist/admin/surface/goals/GoalsList.js +123 -89
  70. package/dist/admin/surface/goals/data.d.ts +8 -2
  71. package/dist/admin/surface/goals/data.js +11 -3
  72. package/dist/admin/surface/goals/goals.css +10 -13
  73. package/dist/admin/surface/home.model.d.ts +7 -8
  74. package/dist/admin/surface/home.model.js +23 -12
  75. package/dist/admin/surface/newTest/NewTest.js +5 -5
  76. package/dist/admin/surface/pageBlock/parts.js +31 -26
  77. package/dist/admin/surface/readiness/ActionDialogs.js +43 -14
  78. package/dist/admin/surface/readiness/Banner.d.ts +3 -0
  79. package/dist/admin/surface/readiness/Banner.js +63 -36
  80. package/dist/admin/surface/readiness/SaveStatus.d.ts +1 -0
  81. package/dist/admin/surface/readiness/SaveStatus.js +6 -3
  82. package/dist/admin/surface/readiness/readiness.model.d.ts +2 -0
  83. package/dist/admin/surface/readiness/readiness.model.js +18 -9
  84. package/dist/admin/surface/readiness/useTransition.d.ts +1 -0
  85. package/dist/admin/surface/readiness/useTransition.js +2 -1
  86. package/dist/admin/surface/results/ReleasePages.d.ts +14 -0
  87. package/dist/admin/surface/results/ReleasePages.js +116 -0
  88. package/dist/admin/surface/results/ResultsBody.d.ts +15 -2
  89. package/dist/admin/surface/results/ResultsBody.js +281 -107
  90. package/dist/admin/surface/results/ResultsHeader.d.ts +10 -0
  91. package/dist/admin/surface/results/ResultsHeader.js +177 -0
  92. package/dist/admin/surface/results/recommendation.model.js +2 -6
  93. package/dist/admin/surface/results/useResults.d.ts +21 -3
  94. package/dist/admin/surface/results/useResults.js +2 -1
  95. package/dist/admin/surface/setting/parts.js +75 -88
  96. package/dist/admin/surface/setting/setting.css +3 -15
  97. package/dist/admin/surface/surface.css +62 -47
  98. package/dist/admin/surface/test/AdvancedSection.js +55 -48
  99. package/dist/admin/surface/test/GoalSection.js +39 -32
  100. package/dist/admin/surface/test/LengthSection.js +46 -34
  101. package/dist/admin/surface/test/Rail.d.ts +4 -0
  102. package/dist/admin/surface/test/Rail.js +83 -0
  103. package/dist/admin/surface/test/ReadinessBar.d.ts +5 -0
  104. package/dist/admin/surface/test/ReadinessBar.js +33 -0
  105. package/dist/admin/surface/test/Section.d.ts +1 -1
  106. package/dist/admin/surface/test/Section.js +36 -16
  107. package/dist/admin/surface/test/TargetingSection.js +168 -142
  108. package/dist/admin/surface/test/TestPage.d.ts +2 -1
  109. package/dist/admin/surface/test/TestPage.js +146 -59
  110. package/dist/admin/surface/test/VersionsSection.js +151 -120
  111. package/dist/admin/surface/test/fields.d.ts +11 -4
  112. package/dist/admin/surface/test/fields.js +84 -85
  113. package/dist/admin/surface/test/jumpToSection.d.ts +3 -0
  114. package/dist/admin/surface/test/jumpToSection.js +10 -0
  115. package/dist/admin/surface/test/pageParts.js +9 -3
  116. package/dist/admin/surface/test/testPage.css +81 -14
  117. package/dist/admin/surface/useTestDocument.d.ts +1 -0
  118. package/dist/admin/surface/useTestDocument.js +40 -9
  119. package/dist/admin/targeting/targeting.model.d.ts +1 -3
  120. package/dist/admin/targeting/targeting.model.js +3 -8
  121. package/dist/admin/theme.css +115 -36
  122. package/dist/admin/theme.d.ts +38 -0
  123. package/dist/admin/theme.js +38 -1
  124. package/dist/admin/useApi.d.ts +17 -2
  125. package/dist/admin/views/ExperimentsRootView.js +12 -2
  126. package/dist/collections/experiments/config.js +44 -1
  127. package/dist/collections/experiments/lockHook.js +16 -2
  128. package/dist/collections/identities.js +6 -1
  129. package/dist/collections/metrics.js +7 -0
  130. package/dist/collections/metricsGuards.d.ts +9 -0
  131. package/dist/collections/metricsGuards.js +33 -0
  132. package/dist/constants.d.ts +1 -0
  133. package/dist/constants.js +1 -0
  134. package/dist/contentDone.d.ts +4 -0
  135. package/dist/contentDone.js +5 -0
  136. package/dist/db/identities.d.ts +8 -7
  137. package/dist/db/identities.js +9 -6
  138. package/dist/db/metricUsage.d.ts +19 -0
  139. package/dist/db/metricUsage.js +54 -0
  140. package/dist/db/names.d.ts +5 -0
  141. package/dist/db/names.js +5 -0
  142. package/dist/db/publishedContent.d.ts +10 -0
  143. package/dist/db/publishedContent.js +43 -0
  144. package/dist/db/results.d.ts +2 -0
  145. package/dist/db/results.js +6 -2
  146. package/dist/db/snapshots.d.ts +18 -6
  147. package/dist/db/snapshots.js +23 -7
  148. package/dist/db/staleEdit.d.ts +6 -0
  149. package/dist/db/staleEdit.js +10 -0
  150. package/dist/db/sweep.js +11 -6
  151. package/dist/endpoints/events.js +3 -3
  152. package/dist/endpoints/index.js +19 -1
  153. package/dist/endpoints/recompute.js +2 -1
  154. package/dist/endpoints/results.js +5 -0
  155. package/dist/endpoints/sanitizeReadinessDetail.d.ts +1 -0
  156. package/dist/endpoints/sanitizeReadinessDetail.js +2 -0
  157. package/dist/endpoints/summary.js +20 -2
  158. package/dist/endpoints/transitions.d.ts +15 -0
  159. package/dist/endpoints/transitions.js +36 -0
  160. package/dist/errors.d.ts +8 -0
  161. package/dist/errors.js +28 -0
  162. package/dist/exports/adminClient.d.ts +1 -0
  163. package/dist/exports/adminClient.js +1 -0
  164. package/dist/exports/index.d.ts +2 -1
  165. package/dist/exports/index.js +1 -0
  166. package/dist/exports/puckEditor.d.ts +1 -0
  167. package/dist/exports/puckEditor.js +1 -0
  168. package/dist/exports/rsc.d.ts +5 -1
  169. package/dist/exports/rsc.js +2 -0
  170. package/dist/hooks/hostHook.js +3 -2
  171. package/dist/hooks/pageGuard.d.ts +5 -0
  172. package/dist/hooks/pageGuard.js +121 -0
  173. package/dist/labels/arms.d.ts +7 -0
  174. package/dist/labels/arms.js +7 -0
  175. package/dist/log.d.ts +7 -0
  176. package/dist/log.js +38 -0
  177. package/dist/manifest/compile.d.ts +5 -0
  178. package/dist/manifest/compile.js +5 -1
  179. package/dist/options.js +23 -0
  180. package/dist/plugin.js +26 -2
  181. package/dist/puck/copy.d.ts +1 -1
  182. package/dist/puck/copy.js +6 -6
  183. package/dist/puck/editor/editor.chrome.css +235 -0
  184. package/dist/puck/editor/editor.css +241 -43
  185. package/dist/results/aggregate.js +4 -3
  186. package/dist/results/analyse.d.ts +4 -1
  187. package/dist/results/analyse.js +27 -11
  188. package/dist/results/cuped.js +3 -2
  189. package/dist/results/identityCredit.d.ts +15 -7
  190. package/dist/results/identityCredit.js +32 -33
  191. package/dist/results/rolloutUnits.js +2 -1
  192. package/dist/results/snapshot.js +8 -2
  193. package/dist/results/srm.js +2 -1
  194. package/dist/results/verdict.d.ts +2 -0
  195. package/dist/results/verdict.js +18 -1
  196. package/dist/sanitizeReadinessDetail.d.ts +7 -0
  197. package/dist/sanitizeReadinessDetail.js +54 -0
  198. package/dist/server/linkIdentity.d.ts +10 -1
  199. package/dist/server/linkIdentity.js +20 -18
  200. package/dist/server/metadata.d.ts +5 -0
  201. package/dist/server/metadata.js +9 -1
  202. package/dist/server/recordContentChange.js +2 -1
  203. package/dist/server/recordExposures.js +3 -3
  204. package/dist/server/runResults.d.ts +3 -2
  205. package/dist/server/runResults.js +125 -34
  206. package/dist/server/trackConversion.js +5 -6
  207. package/dist/targeting/values.d.ts +4 -0
  208. package/dist/targeting/values.js +10 -0
  209. package/dist/transitions/addMetric.js +8 -0
  210. package/dist/transitions/archive.js +2 -3
  211. package/dist/transitions/checkStart.d.ts +6 -3
  212. package/dist/transitions/checkStart.js +15 -0
  213. package/dist/transitions/contentFields.d.ts +7 -0
  214. package/dist/transitions/contentFields.js +20 -0
  215. package/dist/transitions/decide.js +6 -1
  216. package/dist/transitions/deleteExperiment.js +2 -1
  217. package/dist/transitions/dismissContentEntry.d.ts +8 -0
  218. package/dist/transitions/dismissContentEntry.js +45 -0
  219. package/dist/transitions/makeContent.d.ts +5 -2
  220. package/dist/transitions/makeContent.js +217 -118
  221. package/dist/transitions/phaseRecord.d.ts +8 -2
  222. package/dist/transitions/placements.d.ts +1 -0
  223. package/dist/transitions/placements.js +4 -0
  224. package/dist/transitions/preflightData.d.ts +1 -1
  225. package/dist/transitions/preflightData.js +42 -4
  226. package/dist/transitions/releasePages.d.ts +8 -0
  227. package/dist/transitions/releasePages.js +26 -0
  228. package/dist/transitions/restoreContentEntry.d.ts +8 -0
  229. package/dist/transitions/restoreContentEntry.js +47 -0
  230. package/dist/transitions/start.js +30 -5
  231. package/dist/types/contracts.d.ts +14 -0
  232. package/dist/types/endpoints.d.ts +34 -3
  233. package/dist/types/index.d.ts +1 -0
  234. package/dist/types/index.js +1 -0
  235. package/dist/types/reasons.d.ts +3 -0
  236. package/dist/types/reasons.js +1 -0
  237. package/dist/types/snapshot.d.ts +6 -1
  238. package/dist/ui/Banner.d.ts +2 -0
  239. package/dist/ui/Banner.js +2 -0
  240. package/dist/ui/Breadcrumb.d.ts +7 -0
  241. package/dist/ui/Breadcrumb.js +25 -0
  242. package/dist/ui/Button.d.ts +15 -0
  243. package/dist/ui/Button.js +32 -0
  244. package/dist/ui/Card.d.ts +12 -0
  245. package/dist/ui/Card.js +40 -0
  246. package/dist/ui/Checkbox.d.ts +2 -0
  247. package/dist/ui/Checkbox.js +2 -0
  248. package/dist/{admin/components → ui}/ChipsInput.d.ts +4 -3
  249. package/dist/{admin/components → ui}/ChipsInput.js +20 -12
  250. package/dist/ui/Dialog.d.ts +13 -0
  251. package/dist/ui/Dialog.js +85 -0
  252. package/dist/ui/EmptyState.d.ts +2 -0
  253. package/dist/ui/EmptyState.js +2 -0
  254. package/dist/ui/Feedback.d.ts +24 -0
  255. package/dist/ui/Feedback.js +40 -0
  256. package/dist/ui/Inputs.d.ts +34 -0
  257. package/dist/ui/Inputs.js +225 -0
  258. package/dist/ui/NumberInput.d.ts +2 -0
  259. package/dist/ui/NumberInput.js +2 -0
  260. package/dist/ui/Range.d.ts +2 -0
  261. package/dist/ui/Range.js +2 -0
  262. package/dist/ui/SearchInput.d.ts +2 -0
  263. package/dist/ui/SearchInput.js +2 -0
  264. package/dist/ui/SectionCard.d.ts +2 -0
  265. package/dist/ui/SectionCard.js +2 -0
  266. package/dist/ui/Select.d.ts +2 -0
  267. package/dist/ui/Select.js +2 -0
  268. package/dist/ui/StatusChip.d.ts +2 -0
  269. package/dist/ui/StatusChip.js +2 -0
  270. package/dist/ui/Tabs.d.ts +14 -0
  271. package/dist/ui/Tabs.js +54 -0
  272. package/dist/ui/TextInput.d.ts +2 -0
  273. package/dist/ui/TextInput.js +2 -0
  274. package/dist/ui/Textarea.d.ts +2 -0
  275. package/dist/ui/Textarea.js +2 -0
  276. package/dist/ui/Toggle.d.ts +2 -0
  277. package/dist/ui/Toggle.js +2 -0
  278. package/dist/ui/accent.d.ts +21 -0
  279. package/dist/ui/accent.js +81 -0
  280. package/dist/ui/index.d.ts +9 -0
  281. package/dist/ui/index.js +9 -0
  282. package/dist/ui/ui.css +92 -0
  283. package/package.json +1 -1
  284. package/dist/admin/components/chipsInput.css +0 -11
  285. package/dist/admin/results/ArmTable.d.ts +0 -8
  286. package/dist/admin/results/ArmTable.js +0 -113
  287. package/dist/admin/results/VerdictCard.d.ts +0 -9
  288. package/dist/admin/surface/goals/GoalsBreadcrumb.d.ts +0 -5
  289. package/dist/admin/surface/goals/GoalsBreadcrumb.js +0 -47
  290. package/dist/admin/surface/test/TestHeader.d.ts +0 -4
  291. package/dist/admin/surface/test/TestHeader.js +0 -69
@@ -0,0 +1,26 @@
1
+ import { relationshipId } from './checkStart.js';
2
+ import { withTransition } from './kernel.js';
3
+ import { readExperiment, refuse, updateExperiment } from './start.js';
4
+ export async function releasePages(req, { experimentId }) {
5
+ const before = await readExperiment(req, experimentId);
6
+ const { result: doc, revision } = await withTransition(req, {
7
+ experimentId,
8
+ layerId: relationshipId(before.layer)
9
+ }, async ({ req })=>{
10
+ const current = await readExperiment(req, experimentId);
11
+ const decision = current.decision;
12
+ if (current.kind !== 'page' || current.status !== 'archived' || !decision?.winnerArm || decision.discardedAt || decision.pagesReleasedAt) refuse('invalid-status');
13
+ const at = new Date().toISOString();
14
+ return updateExperiment(req, experimentId, {
15
+ decision: {
16
+ ...decision,
17
+ pagesReleasedAt: at,
18
+ pagesReleasedBy: req.user ? String(req.user.id) : null
19
+ }
20
+ });
21
+ });
22
+ return {
23
+ doc,
24
+ revision: revision
25
+ };
26
+ }
@@ -0,0 +1,8 @@
1
+ import type { PayloadRequest } from 'payload';
2
+ import type { DocId } from '../types/contracts.js';
3
+ import type { RestoreContentEntryBody } from '../types/endpoints.js';
4
+ export declare function restoreContentEntry(req: PayloadRequest, { experimentId, collection, id }: RestoreContentEntryBody & {
5
+ experimentId: DocId;
6
+ }): Promise<{
7
+ doc: import("payload").JsonObject & import("payload").TypeWithID;
8
+ }>;
@@ -0,0 +1,47 @@
1
+ import { relationshipId } from './checkStart.js';
2
+ import { withTransition } from './kernel.js';
3
+ import { readExperiment, refuse, updateExperiment } from './start.js';
4
+ export async function restoreContentEntry(req, { experimentId, collection, id }) {
5
+ const before = await readExperiment(req, experimentId);
6
+ // Only the audit report changes; assignment and the manifest stay the same.
7
+ const { result: doc } = await withTransition(req, {
8
+ experimentId,
9
+ layerId: relationshipId(before.layer),
10
+ flagPointId: before.kind === 'flag' ? before.flagPoint : undefined,
11
+ bump: false
12
+ }, async ({ req })=>{
13
+ const current = await readExperiment(req, experimentId);
14
+ const decision = current.decision;
15
+ const report = decision?.contentReport;
16
+ const index = Array.isArray(report?.docs) ? report.docs.findIndex((entry)=>entry.collection === collection && String(entry.id) === String(id)) : -1;
17
+ const entry = report?.docs?.[index];
18
+ if (current.status !== 'decided' || !decision?.winnerArm || decision.discardedAt) refuse('not-decided');
19
+ if (!entry) refuse('entry-not-found');
20
+ if (!entry.dismissedAt) refuse('entry-not-dismissed');
21
+ const docs = report.docs.map((entry, i)=>{
22
+ if (i !== index) return entry;
23
+ const { dismissedAt: _at, dismissedBy: _by, ...restored } = entry;
24
+ return {
25
+ ...restored,
26
+ ok: false
27
+ };
28
+ });
29
+ return updateExperiment(req, experimentId, {
30
+ decision: {
31
+ ...decision,
32
+ contentReport: {
33
+ ...report,
34
+ docs
35
+ }
36
+ }
37
+ });
38
+ });
39
+ req.payload.logger.info({
40
+ experimentId,
41
+ collection,
42
+ id
43
+ }, 'Experiment content entry restored');
44
+ return {
45
+ doc
46
+ };
47
+ }
@@ -1,4 +1,7 @@
1
+ import { lockMetricsForFreeze } from '../db/metricUsage.js';
2
+ import { DEFAULT_SEQUENTIAL_TUNING_PARAMETER } from '../constants.js';
1
3
  import { claimSlices } from '../db/slices.js';
4
+ import { unpublishedPageCopy } from '../admin/copy/readiness.js';
2
5
  import { XpRefusal } from '../errors.js';
3
6
  import { isOpValid } from '../targeting/ops.js';
4
7
  import { newSeed } from '../runtime/seed.js';
@@ -27,21 +30,31 @@ export async function readExperiment(req, id) {
27
30
  }
28
31
  export function refuse(code, detail) {
29
32
  const messages = {
33
+ 'page-target-unpublished': unpublishedPageCopy(typeof detail === 'string' ? detail : 'This page'),
30
34
  'invalid-status': 'This action is not available in the current status.',
31
35
  'stale-draft': 'The experiment changed. Review it again before starting.',
32
36
  'flag-point-busy': 'Another experiment is running or decided on this flag point.',
33
37
  'layer-full': 'There is not enough room in this layer.',
34
38
  'allocation-lower': 'Allocation can only increase while running. Stop and start a new round to lower it.',
35
- 'invalid-body': 'Check the experiment settings and try again.'
39
+ 'invalid-body': 'Check the experiment settings and try again.',
40
+ 'not-decided': 'This test has no standing winner.',
41
+ 'entry-not-found': 'That page is not in the report.',
42
+ 'entry-ok': 'That page was already updated.',
43
+ 'entry-dismissed': 'That page was already dismissed.',
44
+ 'entry-not-dismissed': 'That page is not dismissed.'
36
45
  };
37
46
  const conflict = [
38
47
  'invalid-status',
39
48
  'stale-draft',
40
49
  'flag-point-busy',
41
50
  'layer-full',
42
- 'allocation-lower'
51
+ 'allocation-lower',
52
+ 'not-decided',
53
+ 'entry-ok',
54
+ 'entry-dismissed',
55
+ 'entry-not-dismissed'
43
56
  ].includes(code);
44
- throw new XpRefusal(code, messages[code] ?? 'The experiment is not ready to start.', detail, conflict ? 409 : 422);
57
+ throw new XpRefusal(code, messages[code] ?? 'The experiment is not ready to start.', detail, code === 'entry-not-found' ? 404 : conflict ? 409 : 422);
45
58
  }
46
59
  export async function updateExperiment(req, id, data) {
47
60
  // §7.8: access-authorised transition writes locked plugin fields.
@@ -78,8 +91,10 @@ export async function start(req, { experimentId, options }) {
78
91
  kind: full.kind,
79
92
  analysis: full.analysis
80
93
  };
94
+ // Warning-only codes always have ok: true. These exclusions narrow
95
+ // ReadinessCode to RefusalCode, which intentionally excludes warnings.
81
96
  const failed = checkStart(input).find((check)=>!check.ok);
82
- if (failed) refuse(failed.code, failed.detail);
97
+ if (failed && failed.code !== 'page-targets-unchecked' && failed.code !== 'make-content-unsupported') refuse(failed.code, failed.detail);
83
98
  const before = input.experiment;
84
99
  const updatedAt = before.updatedAt;
85
100
  const at = new Date();
@@ -134,8 +149,17 @@ export async function start(req, { experimentId, options }) {
134
149
  ...input,
135
150
  experiment: current
136
151
  }).find((check)=>!check.ok);
137
- if (recheck) refuse(recheck.code, recheck.detail);
152
+ if (recheck && recheck.code !== 'page-targets-unchecked' && recheck.code !== 'make-content-unsupported') refuse(recheck.code, recheck.detail);
138
153
  }
154
+ const lockedMetrics = await lockMetricsForFreeze(exec, caps.definitions.map((metric)=>metric.id));
155
+ if (!lockedMetrics) {
156
+ throw new XpRefusal('stale-draft', 'A goal in this test was deleted. Review the goals before starting.', undefined, 409);
157
+ }
158
+ // Reachable: a slug/source change takes FOR NO KEY UPDATE, which a draft's FK check does not wait on.
159
+ if (caps.definitions.some((metric)=>{
160
+ const locked = lockedMetrics.find((row)=>String(row.id) === String(metric.id));
161
+ return locked.slug !== metric.slug || locked.source !== metric.source;
162
+ })) throw new XpRefusal('stale-draft', 'A goal changed. Review it again before starting.', undefined, 409);
139
163
  const targeting = (current.targeting ?? []).map((rule)=>{
140
164
  const type = Object.hasOwn(options.attributeKeys ?? {}, rule.attribute) ? options.attributeKeys[rule.attribute].type : undefined;
141
165
  const validValue = type === 'string' && [
@@ -183,6 +207,7 @@ export async function start(req, { experimentId, options }) {
183
207
  ...current.analysis,
184
208
  method: current.analysis?.method ?? 'sequential',
185
209
  alpha: 0.05,
210
+ sequentialTuningParameter: current.analysis?.sequentialTuningParameter ?? DEFAULT_SEQUENTIAL_TUNING_PARAMETER,
186
211
  cupedLookbackDays: 14,
187
212
  layerSlices,
188
213
  minSampleSizes: Object.fromEntries(definitions.map((metric)=>[
@@ -105,6 +105,9 @@ export interface ExperimentsPluginOptions<TUser extends {
105
105
  readResults?: ExperimentsAccess;
106
106
  readIdentities?: ExperimentsAccess;
107
107
  };
108
+ theme?: {
109
+ accent?: string;
110
+ };
108
111
  admin?: {
109
112
  editorUrl?: (args: {
110
113
  collection: string;
@@ -131,6 +134,17 @@ export interface ExperimentsPluginOptions<TUser extends {
131
134
  type: 'string' | 'number' | 'boolean';
132
135
  values?: string[];
133
136
  }>;
137
+ pages?: {
138
+ collections: Array<{
139
+ slug: string;
140
+ pathOf: (doc: Record<string, unknown>) => string | null;
141
+ /** Published-only lookup using the supplied req and overrideAccess: false. Results checks supply an anonymous req. */
142
+ findByPath: (path: string, req: PayloadRequest) => Promise<{
143
+ id: string | number;
144
+ published: boolean;
145
+ } | null>;
146
+ }>;
147
+ };
134
148
  puck?: {
135
149
  enabled: boolean;
136
150
  collections: Array<string | {
@@ -1,10 +1,13 @@
1
- import type { ArmKind, DocId, ExposurePolicy, MetricSource, ResolvedOptions } from './contracts.js';
1
+ import type { ArmKind, DocId, ExperimentKind, ExposurePolicy, MetricSource, ResolvedOptions, Status } from './contracts.js';
2
+ import type { ContentReportReason } from './reasons.js';
2
3
  import type { ArmSlot } from './puck.js';
3
4
  import type { ReadinessCode } from '../transitions/checkStart.js';
4
5
  export type { ReadinessCode } from '../transitions/checkStart.js';
5
6
  import type { Snapshot, VerdictCode } from './snapshot.js';
6
7
  export type ArmIssueCode = 'weights-total' | 'control-count' | 'control-first' | 'holdout-last' | 'holdout-count' | 'variant-count' | 'duplicate-key' | 'duplicate-slot' | 'slot-required' | 'invalid-key';
7
- export type RefusalCode = ArmIssueCode | 'not-found' | 'no-planned-duration' | 'no-primary-metric' | 'no-placements' | 'empty-slot' | 'page-no-targets' | 'page-user-unit' | 'flag-no-point' | 'flag-params-invalid' | 'flag-point-busy' | 'flag-placement-missing' | 'layer-full' | 'layer-unit-mismatch' | 'unknown-metric-source' | 'unit-source-mismatch' | 'no-reach-per-day' | 'invalid-status' | 'stale-draft' | 'commit-unverified' | 'allocation-lower' | 'confirmation-required' | 'invalid-winner' | 'slices-taken' | 'locked-placement' | 'not-implemented' | 'forbidden' | 'unauthenticated' | 'body-too-large' | 'invalid-body' | 'too-many-ids' | 'invalid-phase' | 'invalid-ids' | 'recompute-failed' | 'analysis-failed' | 'undeclared-targeting-key' | 'invalid-targeting-rule' | 'duplicate-metric';
8
+ /** Content-report dismiss/restore refusals (CORE-44). */
9
+ export type ContentEntryRefusal = 'not-decided' | 'entry-not-found' | 'entry-ok' | 'entry-dismissed' | 'entry-not-dismissed';
10
+ export type RefusalCode = ArmIssueCode | 'not-found' | ContentEntryRefusal | 'no-planned-duration' | 'no-primary-metric' | 'no-placements' | 'empty-slot' | 'page-no-targets' | 'page-target-unpublished' | 'page-user-unit' | 'version-page-unavailable' | 'version-page-unchecked' | 'flag-no-point' | 'flag-params-invalid' | 'flag-point-busy' | 'flag-placement-missing' | 'layer-full' | 'layer-unit-mismatch' | 'unknown-metric-source' | 'unit-source-mismatch' | 'no-reach-per-day' | 'invalid-status' | 'stale-draft' | 'stale-edit' | 'commit-unverified' | 'allocation-lower' | 'confirmation-required' | 'invalid-winner' | 'slices-taken' | 'locked-placement' | 'not-implemented' | 'forbidden' | 'unauthenticated' | 'body-too-large' | 'invalid-body' | 'too-many-ids' | 'invalid-phase' | 'invalid-ids' | 'recompute-failed' | 'analysis-failed' | 'undeclared-targeting-key' | 'invalid-targeting-rule' | 'block-path-rule' | 'duplicate-metric';
8
11
  export type ErrorEnvelope = {
9
12
  errors: Array<{
10
13
  code: RefusalCode;
@@ -32,12 +35,25 @@ export type ArchiveBody = {
32
35
  mode: 'stopped' | 'after-content' | 'discard' | 'hard-coded';
33
36
  confirmation?: string;
34
37
  };
38
+ export type DismissContentEntryBody = {
39
+ collection: string;
40
+ id: DocId;
41
+ };
42
+ export type DismissContentEntryResult = Pick<TransitionSuccess, 'doc'>;
43
+ export type RestoreContentEntryBody = DismissContentEntryBody;
44
+ export type RestoreContentEntryResult = Pick<TransitionSuccess, 'doc'>;
45
+ export type ContentReport = {
46
+ docs: MakeContentResult['docs'];
47
+ } | MakeContentResult['docs'];
35
48
  export type MakeContentResult = TransitionSuccess & {
36
49
  docs: Array<{
37
50
  collection: string;
38
51
  id: DocId;
39
52
  ok: boolean;
40
- error?: string;
53
+ detail?: ContentReportReason | string;
54
+ error?: ContentReportReason | string;
55
+ dismissedAt?: string;
56
+ dismissedBy?: string | null;
41
57
  }>;
42
58
  };
43
59
  export type PreflightResult = {
@@ -45,6 +61,7 @@ export type PreflightResult = {
45
61
  code: ReadinessCode;
46
62
  ok: boolean;
47
63
  detail?: unknown;
64
+ warning?: boolean;
48
65
  }>;
49
66
  placements: Array<{
50
67
  collection: string;
@@ -93,6 +110,12 @@ export interface PhaseSummary {
93
110
  }>;
94
111
  }
95
112
  export type ResultsResponse = {
113
+ /** Filled by the results endpoint from CORE-48: what a results reader may see about the test. */
114
+ experiment?: {
115
+ name: string;
116
+ kind: ExperimentKind;
117
+ status: Status;
118
+ };
96
119
  phases: PhaseSummary[];
97
120
  snapshot: Snapshot | null;
98
121
  decisionSnapshot?: Snapshot;
@@ -110,6 +133,14 @@ export type ResultsResponse = {
110
133
  };
111
134
  export type SummaryResponse = Record<DocId, {
112
135
  ready?: boolean;
136
+ next?: {
137
+ code: ReadinessCode;
138
+ detail?: unknown;
139
+ };
140
+ health?: Array<{
141
+ code: 'version-page-unavailable' | 'version-page-unchecked';
142
+ path: string;
143
+ }>;
113
144
  exposures: number;
114
145
  days: number;
115
146
  verdict: {
@@ -6,3 +6,4 @@ export * from './puck.js';
6
6
  export * from './admin.js';
7
7
  export * from './endpoints.js';
8
8
  export * from './snapshot.js';
9
+ export * from './reasons.js';
@@ -6,3 +6,4 @@ export * from './puck.js';
6
6
  export * from './admin.js';
7
7
  export * from './endpoints.js';
8
8
  export * from './snapshot.js';
9
+ export * from './reasons.js';
@@ -0,0 +1,3 @@
1
+ /** Closed codes written by new results runs; stored snapshots may predate them. */
2
+ export type MetricUnavailableReason = 'source-failed' | 'source-unavailable' | 'unit-type-unavailable' | 'non-finite-values' | 'non-finite-covariates' | 'budget' | 'not-supplied' | 'analysis-failed';
3
+ export type ContentReportReason = 'localized' | 'locked' | 'forbidden' | 'not-found' | 'validation' | 'update-failed';
@@ -0,0 +1 @@
1
+ /** Closed codes written by new results runs; stored snapshots may predate them. */ export { };
@@ -1,5 +1,6 @@
1
1
  import type { GaussianPrior } from '@delmaredigital/experiments-stats';
2
2
  import type { DocId, MetricSource } from './contracts.js';
3
+ import type { MetricUnavailableReason } from './reasons.js';
3
4
  export type VerdictCode = 'unavailable' | 'data-problem' | 'guardrail-failing' | 'collecting' | 'original-better' | 'worse-none-better' | 'winner' | 'mixed' | 'keep-running' | 'no-difference' | 'rollout-collecting' | 'rollout-readout';
4
5
  export interface Snapshot {
5
6
  experiment: DocId;
@@ -54,7 +55,7 @@ export interface Snapshot {
54
55
  kind: MetricSource['kind'];
55
56
  role: 'primary' | 'secondary' | 'guardrail';
56
57
  cupedApplied: boolean;
57
- unavailable?: string;
58
+ unavailable?: MetricUnavailableReason | string;
58
59
  perArm: Array<{
59
60
  arm: string;
60
61
  n: number;
@@ -107,6 +108,10 @@ export interface Snapshot {
107
108
  guardrailLine?: string;
108
109
  worstArm?: string;
109
110
  };
111
+ health?: Array<{
112
+ code: 'version-page-unavailable' | 'version-page-unchecked';
113
+ path: string;
114
+ }>;
110
115
  contentChanges: Array<{
111
116
  at: string;
112
117
  doc: {
@@ -0,0 +1,2 @@
1
+ import './ui.css';
2
+ export { Banner } from './Feedback.js';
@@ -0,0 +1,2 @@
1
+ import './ui.css';
2
+ export { Banner } from './Feedback.js';
@@ -0,0 +1,7 @@
1
+ import './ui.css';
2
+ export declare function Breadcrumb({ items }: {
3
+ items: Array<{
4
+ label: string;
5
+ href?: string;
6
+ }>;
7
+ }): import("react").JSX.Element;
@@ -0,0 +1,25 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import './ui.css';
3
+ export function Breadcrumb({ items }) {
4
+ return /*#__PURE__*/ _jsx("nav", {
5
+ className: "xp-ui-breadcrumb",
6
+ "aria-label": "Breadcrumb",
7
+ children: /*#__PURE__*/ _jsx("ol", {
8
+ children: items.map((item, index)=>/*#__PURE__*/ _jsxs("li", {
9
+ children: [
10
+ index ? /*#__PURE__*/ _jsx("span", {
11
+ "aria-hidden": "true",
12
+ children: "›"
13
+ }) : null,
14
+ index === items.length - 1 ? /*#__PURE__*/ _jsx("span", {
15
+ "aria-current": "page",
16
+ children: item.label
17
+ }) : /*#__PURE__*/ _jsx("a", {
18
+ href: item.href,
19
+ children: item.label
20
+ })
21
+ ]
22
+ }, index))
23
+ })
24
+ });
25
+ }
@@ -0,0 +1,15 @@
1
+ import type { AnchorHTMLAttributes, ButtonHTMLAttributes } from 'react';
2
+ import './ui.css';
3
+ type Shared = {
4
+ variant?: 'primary' | 'secondary' | 'quiet' | 'link' | 'danger';
5
+ size?: 'md' | 'sm';
6
+ disabled?: boolean;
7
+ busy?: boolean;
8
+ };
9
+ export type ButtonProps = Shared & (ButtonHTMLAttributes<HTMLButtonElement> & {
10
+ href?: never;
11
+ } | AnchorHTMLAttributes<HTMLAnchorElement> & {
12
+ href: string;
13
+ });
14
+ export declare function Button({ variant, size, disabled, busy, className, children, ...props }: ButtonProps): import("react").JSX.Element;
15
+ export {};
@@ -0,0 +1,32 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import './ui.css';
4
+ export function Button({ variant = 'secondary', size = 'md', disabled, busy, className = '', children, ...props }) {
5
+ const classes = `xp-ui-button xp-ui-button--${variant} xp-ui-button--${size} ${className}`;
6
+ const blocked = disabled || busy;
7
+ if (props.href !== undefined) {
8
+ const { href, onClick, ...rest } = props;
9
+ return /*#__PURE__*/ _jsx("a", {
10
+ ...rest,
11
+ className: classes,
12
+ href: blocked ? undefined : href,
13
+ role: blocked ? 'link' : undefined,
14
+ "aria-disabled": blocked || undefined,
15
+ "aria-busy": busy || undefined,
16
+ tabIndex: blocked ? -1 : rest.tabIndex,
17
+ onClick: (event)=>{
18
+ if (blocked) event.preventDefault();
19
+ else onClick?.(event);
20
+ },
21
+ children: children
22
+ });
23
+ }
24
+ return /*#__PURE__*/ _jsx("button", {
25
+ ...props,
26
+ type: props.type ?? 'button',
27
+ className: classes,
28
+ disabled: blocked,
29
+ "aria-busy": busy || undefined,
30
+ children: children
31
+ });
32
+ }
@@ -0,0 +1,12 @@
1
+ import type { HTMLAttributes, ReactNode } from 'react';
2
+ import './ui.css';
3
+ export declare function Card({ className, ...props }: HTMLAttributes<HTMLDivElement>): import("react").JSX.Element;
4
+ export declare function SectionCard({ number, title, complete, children, id, headingId, className }: {
5
+ number: number;
6
+ title: string;
7
+ complete?: boolean;
8
+ children: ReactNode;
9
+ id?: string;
10
+ headingId?: string;
11
+ className?: string;
12
+ }): import("react").JSX.Element;
@@ -0,0 +1,40 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import './ui.css';
3
+ export function Card({ className = '', ...props }) {
4
+ return /*#__PURE__*/ _jsx("div", {
5
+ ...props,
6
+ className: `xp-ui-card ${className}`
7
+ });
8
+ }
9
+ export function SectionCard({ number, title, complete = false, children, id, headingId, className = '' }) {
10
+ return /*#__PURE__*/ _jsxs("section", {
11
+ id: id,
12
+ "aria-labelledby": headingId,
13
+ className: `xp-ui-card xp-ui-section ${className}`,
14
+ children: [
15
+ /*#__PURE__*/ _jsxs("header", {
16
+ className: "xp-ui-section-head",
17
+ children: [
18
+ /*#__PURE__*/ _jsx("span", {
19
+ className: "xp-ui-section-number",
20
+ children: number
21
+ }),
22
+ /*#__PURE__*/ _jsx("h2", {
23
+ id: headingId,
24
+ tabIndex: -1,
25
+ children: title
26
+ }),
27
+ /*#__PURE__*/ _jsx("span", {
28
+ className: "xp-ui-section-complete",
29
+ "aria-label": complete ? 'Complete' : 'Incomplete',
30
+ children: complete ? '✓' : '○'
31
+ })
32
+ ]
33
+ }),
34
+ /*#__PURE__*/ _jsx("div", {
35
+ className: "xp-ui-section-body",
36
+ children: children
37
+ })
38
+ ]
39
+ });
40
+ }
@@ -0,0 +1,2 @@
1
+ import './ui.css';
2
+ export { Checkbox, CheckboxList } from './Inputs.js';
@@ -0,0 +1,2 @@
1
+ import './ui.css';
2
+ export { Checkbox, CheckboxList } from './Inputs.js';
@@ -1,12 +1,13 @@
1
- import '../theme.css';
2
- import './chipsInput.css';
1
+ import './ui.css';
3
2
  export interface ChipsInputProps {
4
3
  value: string[];
5
4
  onChange: (value: string[]) => void;
6
5
  label: string;
7
6
  description?: string;
7
+ help?: string;
8
+ error?: string;
8
9
  placeholder?: string;
9
10
  disabled?: boolean;
10
11
  id: string;
11
12
  }
12
- export declare function ChipsInput({ value, onChange, label, description, placeholder, disabled, id }: ChipsInputProps): import("react").JSX.Element;
13
+ export declare function ChipsInput({ value, onChange, label, description, help, error, placeholder, disabled, id }: ChipsInputProps): import("react").JSX.Element;
@@ -1,10 +1,8 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useRef, useState } from 'react';
4
- import { AUDIENCE_COPY } from '../copy/audience.js';
5
- import '../theme.css';
6
- import './chipsInput.css';
7
- export function ChipsInput({ value, onChange, label, description, placeholder, disabled = false, id }) {
4
+ import './ui.css';
5
+ export function ChipsInput({ value, onChange, label, description, help = description, error, placeholder, disabled = false, id }) {
8
6
  const [draft, setDraft] = useState('');
9
7
  const input = useRef(null);
10
8
  const commit = (text)=>{
@@ -24,7 +22,7 @@ export function ChipsInput({ value, onChange, label, description, placeholder, d
24
22
  input.current?.focus();
25
23
  };
26
24
  return /*#__PURE__*/ _jsxs("div", {
27
- className: "xp-chips",
25
+ className: "xp-ui-chips",
28
26
  role: "group",
29
27
  "aria-labelledby": `${id}-label`,
30
28
  "aria-disabled": disabled,
@@ -35,10 +33,10 @@ export function ChipsInput({ value, onChange, label, description, placeholder, d
35
33
  children: label
36
34
  }),
37
35
  /*#__PURE__*/ _jsxs("div", {
38
- className: "xp-chips-control",
36
+ className: `xp-ui-chips-control${error ? ' xp-ui-chips-control--invalid' : ''}`,
39
37
  children: [
40
38
  value.map((item, index)=>/*#__PURE__*/ _jsxs("span", {
41
- className: "xp-chips-item",
39
+ className: "xp-ui-chips-item",
42
40
  children: [
43
41
  /*#__PURE__*/ _jsx("span", {
44
42
  children: item
@@ -46,9 +44,9 @@ export function ChipsInput({ value, onChange, label, description, placeholder, d
46
44
  /*#__PURE__*/ _jsx("button", {
47
45
  type: "button",
48
46
  disabled: disabled,
49
- "aria-label": AUDIENCE_COPY.removeValue(item),
47
+ "aria-label": `Remove ${item}`,
50
48
  onClick: ()=>remove(index),
51
- children: AUDIENCE_COPY.removeSymbol
49
+ children: '×'
52
50
  })
53
51
  ]
54
52
  }, item)),
@@ -59,7 +57,11 @@ export function ChipsInput({ value, onChange, label, description, placeholder, d
59
57
  value: draft,
60
58
  disabled: disabled,
61
59
  placeholder: placeholder,
62
- "aria-describedby": description ? `${id}-description` : undefined,
60
+ "aria-invalid": Boolean(error) || undefined,
61
+ "aria-describedby": [
62
+ help ? `${id}-description` : '',
63
+ error ? `${id}-error` : ''
64
+ ].filter(Boolean).join(' ') || undefined,
63
65
  onChange: (event)=>setDraft(event.target.value),
64
66
  onBlur: ()=>{
65
67
  if (draft.trim()) commit(draft);
@@ -87,9 +89,15 @@ export function ChipsInput({ value, onChange, label, description, placeholder, d
87
89
  })
88
90
  ]
89
91
  }),
90
- description ? /*#__PURE__*/ _jsx("p", {
92
+ help ? /*#__PURE__*/ _jsx("p", {
91
93
  id: `${id}-description`,
92
- children: description
94
+ children: help
95
+ }) : null,
96
+ error ? /*#__PURE__*/ _jsx("p", {
97
+ className: "xp-ui-error",
98
+ id: `${id}-error`,
99
+ role: "alert",
100
+ children: error
93
101
  }) : null
94
102
  ]
95
103
  });
@@ -0,0 +1,13 @@
1
+ import type { ReactNode } from 'react';
2
+ import './ui.css';
3
+ /** Native modal stays in its rendering document, including a Puck canvas iframe.
4
+ * `initialFocus="cancel"` targets the focusable control marked `data-dialog-cancel`.
5
+ */
6
+ export declare function Dialog({ open, title, onClose, children, dismissible, initialFocus }: {
7
+ open: boolean;
8
+ dismissible?: boolean;
9
+ initialFocus?: 'cancel';
10
+ title: string;
11
+ onClose: () => void;
12
+ children: ReactNode;
13
+ }): import("react").JSX.Element;