@delmaredigital/payload-experiments 0.3.0-rc.1 → 0.4.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 (338) hide show
  1. package/README.md +172 -21
  2. package/dist/admin/MetricSourceSelect.js +3 -3
  3. package/dist/admin/PlacementStatus.d.ts +3 -0
  4. package/dist/admin/PlacementStatus.js +54 -0
  5. package/dist/admin/PreviewLinks.d.ts +2 -1
  6. package/dist/admin/PreviewLinks.js +47 -26
  7. package/dist/admin/TestsNavLink.d.ts +3 -1
  8. package/dist/admin/TestsNavLink.js +2 -2
  9. package/dist/admin/actions/ConfirmDialog.d.ts +2 -1
  10. package/dist/admin/actions/ConfirmDialog.js +2 -2
  11. package/dist/admin/actions/DecideDialog.d.ts +2 -1
  12. package/dist/admin/actions/DecideDialog.js +2 -2
  13. package/dist/admin/api.d.ts +11 -2
  14. package/dist/admin/api.js +20 -2
  15. package/dist/admin/armSelect/ArmSelectField.js +14 -22
  16. package/dist/admin/arms/arms.model.d.ts +1 -21
  17. package/dist/admin/arms/arms.model.js +1 -153
  18. package/dist/admin/blocks/BlockPanel.d.ts +4 -0
  19. package/dist/admin/blocks/BlockPanel.js +153 -0
  20. package/dist/admin/blocks/TestBlockField.d.ts +5 -0
  21. package/dist/admin/blocks/TestBlockField.js +163 -0
  22. package/dist/admin/blocks/formApi.d.ts +43 -0
  23. package/dist/admin/blocks/formApi.js +153 -0
  24. package/dist/admin/blocks/inject.d.ts +7 -0
  25. package/dist/admin/blocks/inject.js +41 -0
  26. package/dist/admin/blocks/model.d.ts +17 -0
  27. package/dist/admin/blocks/model.js +45 -0
  28. package/dist/admin/blocks/statusCache.d.ts +7 -0
  29. package/dist/admin/blocks/statusCache.js +83 -0
  30. package/dist/admin/blocks/useBlockForm.d.ts +17 -0
  31. package/dist/admin/blocks/useBlockForm.js +46 -0
  32. package/dist/admin/copy/actions.d.ts +2 -0
  33. package/dist/admin/copy/actions.js +9 -0
  34. package/dist/admin/copy/arms.d.ts +1 -6
  35. package/dist/admin/copy/arms.js +1 -16
  36. package/dist/admin/copy/blocks.d.ts +27 -0
  37. package/dist/admin/copy/blocks.js +38 -0
  38. package/dist/admin/copy/goals.d.ts +2 -0
  39. package/dist/admin/copy/goals.js +4 -1
  40. package/dist/admin/copy/newTest.d.ts +10 -0
  41. package/dist/admin/copy/newTest.js +10 -0
  42. package/dist/admin/copy/pages.d.ts +26 -0
  43. package/dist/admin/copy/pages.js +26 -0
  44. package/dist/admin/copy/readiness.d.ts +25 -3
  45. package/dist/admin/copy/readiness.js +25 -3
  46. package/dist/admin/copy/results.d.ts +3 -0
  47. package/dist/admin/copy/results.js +4 -1
  48. package/dist/admin/copy/review.d.ts +1 -1
  49. package/dist/admin/copy/review.js +1 -1
  50. package/dist/admin/copy/surface.d.ts +0 -1
  51. package/dist/admin/copy/surface.js +0 -1
  52. package/dist/admin/copy/testPage.d.ts +4 -0
  53. package/dist/admin/copy/testPage.js +4 -0
  54. package/dist/admin/goals/XpGoalField.d.ts +2 -0
  55. package/dist/admin/goals/XpGoalField.js +152 -0
  56. package/dist/admin/metricSource.model.d.ts +2 -0
  57. package/dist/admin/metricSource.model.js +4 -0
  58. package/dist/admin/pageCollections.d.ts +10 -0
  59. package/dist/admin/pageCollections.js +20 -0
  60. package/dist/admin/paths.d.ts +8 -1
  61. package/dist/admin/paths.js +8 -1
  62. package/dist/admin/placementStatus.model.d.ts +2 -0
  63. package/dist/admin/placementStatus.model.js +19 -0
  64. package/dist/admin/placements/placementLabels.d.ts +11 -0
  65. package/dist/admin/placements/placementLabels.js +21 -0
  66. package/dist/admin/placements/placementLabels.model.d.ts +13 -0
  67. package/dist/admin/placements/placementLabels.model.js +12 -0
  68. package/dist/admin/placements/usePlacementLabels.d.ts +12 -0
  69. package/dist/admin/placements/usePlacementLabels.js +102 -0
  70. package/dist/admin/previewLinks.model.d.ts +2 -0
  71. package/dist/admin/previewLinks.model.js +3 -0
  72. package/dist/admin/results/DataQualityPanel.js +4 -1
  73. package/dist/admin/results/legacy.d.ts +16 -0
  74. package/dist/admin/results/legacy.js +27 -0
  75. package/dist/admin/results/results.model.d.ts +9 -6
  76. package/dist/admin/results/results.model.js +22 -9
  77. package/dist/admin/results/snapshot.d.ts +14 -0
  78. package/dist/admin/results/snapshot.js +3 -0
  79. package/dist/admin/surface/TestsHome.js +43 -15
  80. package/dist/admin/surface/TestsSurface.js +5 -1
  81. package/dist/admin/surface/contracts.d.ts +6 -5
  82. package/dist/admin/surface/goals/GoalForm.js +67 -34
  83. package/dist/admin/surface/goals/GoalsList.js +3 -3
  84. package/dist/admin/surface/goals/data.js +19 -16
  85. package/dist/admin/surface/goals/goals.model.d.ts +1 -2
  86. package/dist/admin/surface/goals/goals.model.js +1 -3
  87. package/dist/admin/surface/home.model.d.ts +2 -1
  88. package/dist/admin/surface/home.model.js +8 -4
  89. package/dist/admin/surface/newTest/NewTest.js +58 -17
  90. package/dist/admin/surface/newTest/PageWizard.d.ts +7 -0
  91. package/dist/admin/surface/newTest/PageWizard.js +209 -0
  92. package/dist/admin/surface/pagePicker/CopyRoute.d.ts +6 -0
  93. package/dist/admin/surface/pagePicker/CopyRoute.js +53 -0
  94. package/dist/admin/surface/pagePicker/PagePicker.d.ts +10 -0
  95. package/dist/admin/surface/pagePicker/PagePicker.js +172 -0
  96. package/dist/admin/surface/pagePicker/pagePicker.model.d.ts +25 -0
  97. package/dist/admin/surface/pagePicker/pagePicker.model.js +25 -0
  98. package/dist/admin/surface/readiness/ActionDialogs.js +103 -69
  99. package/dist/admin/surface/readiness/Banner.js +30 -6
  100. package/dist/admin/surface/readiness/readiness.model.d.ts +8 -1
  101. package/dist/admin/surface/readiness/readiness.model.js +95 -6
  102. package/dist/admin/surface/results/ReleasePages.d.ts +3 -1
  103. package/dist/admin/surface/results/ReleasePages.js +3 -7
  104. package/dist/admin/surface/results/ResultsBody.js +23 -3
  105. package/dist/admin/surface/results/ResultsHeader.js +2 -5
  106. package/dist/admin/surface/results/recommendation.model.d.ts +3 -2
  107. package/dist/admin/surface/results/recommendation.model.js +10 -3
  108. package/dist/admin/surface/results/useResults.d.ts +10 -1
  109. package/dist/admin/surface/surface.css +0 -1
  110. package/dist/admin/surface/test/AdvancedSection.js +111 -104
  111. package/dist/admin/surface/test/GoalSection.js +97 -22
  112. package/dist/admin/surface/test/LengthSection.js +1 -1
  113. package/dist/admin/surface/test/PageHoldout.d.ts +3 -0
  114. package/dist/admin/surface/test/PageHoldout.js +67 -0
  115. package/dist/admin/surface/test/TestPage.js +2 -1
  116. package/dist/admin/surface/test/VersionsSection.js +12 -5
  117. package/dist/admin/surface/test/fields.d.ts +13 -4
  118. package/dist/admin/surface/test/fields.js +46 -14
  119. package/dist/admin/surface/test/jumpToSection.d.ts +1 -1
  120. package/dist/admin/surface/test/jumpToSection.js +4 -3
  121. package/dist/admin/surface/test/pageParts.d.ts +4 -1
  122. package/dist/admin/surface/test/pageParts.js +79 -2
  123. package/dist/admin/surface/test/testPage.css +8 -1
  124. package/dist/admin/surface/test/testPage.model.d.ts +2 -3
  125. package/dist/admin/surface/test/testPage.model.js +1 -21
  126. package/dist/admin/surface/testDocument.model.d.ts +1 -0
  127. package/dist/admin/surface/testDocument.model.js +2 -1
  128. package/dist/admin/surface/useTestDocument.d.ts +11 -2
  129. package/dist/admin/surface/useTestDocument.js +55 -16
  130. package/dist/admin/useApi.d.ts +11 -2
  131. package/dist/admin/useApi.js +3 -5
  132. package/dist/admin/useLayerUsage.js +14 -23
  133. package/dist/admin/views/ExperimentsRootView.js +58 -16
  134. package/dist/admin/views/TestsNavLinkServer.d.ts +2 -0
  135. package/dist/admin/views/TestsNavLinkServer.js +43 -0
  136. package/dist/arms/copy.d.ts +6 -0
  137. package/dist/arms/copy.js +16 -0
  138. package/dist/arms/model.d.ts +23 -0
  139. package/dist/arms/model.js +173 -0
  140. package/dist/blocks/bindings.d.ts +3 -0
  141. package/dist/blocks/bindings.js +34 -0
  142. package/dist/blocks/createExperimentBlock.d.ts +6 -0
  143. package/dist/blocks/createExperimentBlock.js +77 -0
  144. package/dist/blocks/formState.d.ts +31 -0
  145. package/dist/blocks/formState.js +146 -0
  146. package/dist/blocks/model.d.ts +41 -0
  147. package/dist/blocks/model.js +34 -0
  148. package/dist/client/ExperimentGroup.d.ts +6 -0
  149. package/dist/client/ExperimentGroup.js +9 -0
  150. package/dist/client/ExperimentsProvider.js +20 -4
  151. package/dist/client/clickGoals.d.ts +4 -0
  152. package/dist/client/clickGoals.js +43 -0
  153. package/dist/collections/experiments/config.js +12 -2
  154. package/dist/collections/experiments/deleteGuard.d.ts +2 -1
  155. package/dist/collections/experiments/deleteGuard.js +12 -1
  156. package/dist/collections/experiments/editability.js +4 -2
  157. package/dist/collections/experiments/pickSlug.d.ts +2 -0
  158. package/dist/collections/experiments/pickSlug.js +8 -0
  159. package/dist/collections/labels.d.ts +3 -0
  160. package/dist/collections/labels.js +20 -0
  161. package/dist/collections/metrics.js +12 -2
  162. package/dist/collections/phases.js +1 -0
  163. package/dist/collections/serialize.js +7 -0
  164. package/dist/db/appends.d.ts +6 -2
  165. package/dist/db/appends.js +10 -4
  166. package/dist/db/clickGoals.d.ts +9 -0
  167. package/dist/db/clickGoals.js +22 -0
  168. package/dist/db/delete.d.ts +1 -0
  169. package/dist/db/delete.js +1 -0
  170. package/dist/db/locks.d.ts +14 -0
  171. package/dist/db/locks.js +36 -1
  172. package/dist/db/names.d.ts +4 -0
  173. package/dist/db/names.js +4 -0
  174. package/dist/db/pageBindings.d.ts +4 -0
  175. package/dist/db/pageBindings.js +17 -0
  176. package/dist/db/pageBusy.d.ts +2 -0
  177. package/dist/db/pageBusy.js +10 -0
  178. package/dist/db/pageContent.d.ts +10 -0
  179. package/dist/db/pageContent.js +79 -0
  180. package/dist/db/pageOccupancy.d.ts +12 -0
  181. package/dist/db/pageOccupancy.js +45 -0
  182. package/dist/db/placements.d.ts +33 -0
  183. package/dist/db/placements.js +54 -0
  184. package/dist/db/slugs.d.ts +8 -0
  185. package/dist/db/slugs.js +40 -0
  186. package/dist/db/snapshots.d.ts +6 -0
  187. package/dist/db/snapshots.js +9 -5
  188. package/dist/endpoints/clickKeys.d.ts +5 -0
  189. package/dist/endpoints/clickKeys.js +17 -0
  190. package/dist/endpoints/events.js +4 -1
  191. package/dist/endpoints/index.js +28 -0
  192. package/dist/endpoints/pages.d.ts +5 -0
  193. package/dist/endpoints/pages.js +120 -0
  194. package/dist/endpoints/placementStatus.d.ts +6 -0
  195. package/dist/endpoints/placementStatus.js +238 -0
  196. package/dist/endpoints/readBody.d.ts +2 -0
  197. package/dist/endpoints/readBody.js +24 -0
  198. package/dist/endpoints/results.js +5 -1
  199. package/dist/endpoints/summary.js +11 -4
  200. package/dist/endpoints/wrapBlock.d.ts +6 -0
  201. package/dist/endpoints/wrapBlock.js +76 -0
  202. package/dist/errors.d.ts +3 -3
  203. package/dist/errors.js +4 -4
  204. package/dist/events/rows.d.ts +1 -1
  205. package/dist/events/rows.js +5 -3
  206. package/dist/exports/adminClient.d.ts +5 -0
  207. package/dist/exports/adminClient.js +5 -0
  208. package/dist/exports/adminRsc.d.ts +1 -0
  209. package/dist/exports/adminRsc.js +1 -0
  210. package/dist/exports/blocks.d.ts +2 -0
  211. package/dist/exports/blocks.js +1 -0
  212. package/dist/exports/client.d.ts +1 -0
  213. package/dist/exports/client.js +1 -0
  214. package/dist/exports/index.d.ts +6 -1
  215. package/dist/exports/index.js +3 -0
  216. package/dist/exports/puck.d.ts +5 -2
  217. package/dist/exports/puck.js +3 -1
  218. package/dist/exports/puckEditor.d.ts +6 -1
  219. package/dist/exports/puckEditor.js +4 -1
  220. package/dist/exports/rsc.d.ts +4 -0
  221. package/dist/exports/rsc.js +2 -0
  222. package/dist/fields/xpGoal.d.ts +3 -0
  223. package/dist/fields/xpGoal.js +28 -0
  224. package/dist/globals/state.js +10 -0
  225. package/dist/goals/cache.d.ts +3 -0
  226. package/dist/goals/cache.js +35 -0
  227. package/dist/goals/fairness.d.ts +40 -0
  228. package/dist/goals/fairness.js +200 -0
  229. package/dist/goals/key.d.ts +4 -0
  230. package/dist/goals/key.js +3 -0
  231. package/dist/goals/picker/clickGoalPicker.model.d.ts +29 -0
  232. package/dist/goals/picker/clickGoalPicker.model.js +45 -0
  233. package/dist/goals/picker/copy.d.ts +16 -0
  234. package/dist/goals/picker/copy.js +15 -0
  235. package/dist/goals/picker/useClickGoalPicker.d.ts +30 -0
  236. package/dist/goals/picker/useClickGoalPicker.js +220 -0
  237. package/dist/hooks/pageGuard.d.ts +5 -1
  238. package/dist/hooks/pageGuard.js +160 -80
  239. package/dist/lib/fetchAllDocs.d.ts +16 -0
  240. package/dist/lib/fetchAllDocs.js +56 -0
  241. package/dist/onInit.js +29 -0
  242. package/dist/options.d.ts +2 -0
  243. package/dist/options.js +43 -10
  244. package/dist/plugin.js +45 -28
  245. package/dist/puck/ExperimentPicker.js +47 -16
  246. package/dist/puck/ExperimentSlot.d.ts +51 -1
  247. package/dist/puck/ExperimentSlot.js +23 -12
  248. package/dist/puck/copy.d.ts +6 -4
  249. package/dist/puck/copy.js +6 -4
  250. package/dist/puck/editor/CreateExperimentDrawer.js +46 -56
  251. package/dist/puck/editor/ExperimentSlotEditor.d.ts +52 -1
  252. package/dist/puck/editor/ExperimentSlotEditor.js +292 -137
  253. package/dist/puck/editor/ResultsReadout.d.ts +6 -0
  254. package/dist/puck/editor/ResultsReadout.js +99 -0
  255. package/dist/puck/editor/VersionBuilder.d.ts +34 -0
  256. package/dist/puck/editor/VersionBuilder.js +317 -0
  257. package/dist/puck/editor/armWrites.d.ts +7 -0
  258. package/dist/puck/editor/armWrites.js +71 -0
  259. package/dist/puck/editor/armsCache.d.ts +14 -0
  260. package/dist/puck/editor/armsCache.js +78 -0
  261. package/dist/puck/editor/copy.d.ts +33 -1
  262. package/dist/puck/editor/copy.js +45 -8
  263. package/dist/puck/editor/createTest.d.ts +1 -1
  264. package/dist/puck/editor/createTest.js +25 -14
  265. package/dist/puck/editor/editor.chrome.css +48 -161
  266. package/dist/puck/editor/editor.css +48 -161
  267. package/dist/puck/editor/editor.model.d.ts +8 -3
  268. package/dist/puck/editor/editor.model.js +16 -10
  269. package/dist/puck/editor/experimentOutlinePlugin.d.ts +3 -0
  270. package/dist/puck/editor/experimentOutlinePlugin.js +89 -0
  271. package/dist/puck/editor/integration.d.ts +10 -0
  272. package/dist/puck/editor/integration.js +30 -0
  273. package/dist/puck/editor/xpGoalPuckEditorField.d.ts +7 -0
  274. package/dist/puck/editor/xpGoalPuckEditorField.js +134 -0
  275. package/dist/puck/experimentSlotField.d.ts +2 -0
  276. package/dist/puck/experimentSlotField.js +11 -3
  277. package/dist/puck/hostContext.d.ts +10 -0
  278. package/dist/puck/hostContext.js +12 -0
  279. package/dist/puck/integration.d.ts +6 -0
  280. package/dist/puck/integration.js +21 -0
  281. package/dist/puck/routes.d.ts +12 -0
  282. package/dist/puck/routes.js +11 -0
  283. package/dist/puck/slotConfig.d.ts +1 -1
  284. package/dist/puck/slotConfig.js +2 -2
  285. package/dist/puck/xpGoalField.d.ts +3 -0
  286. package/dist/puck/xpGoalField.js +6 -0
  287. package/dist/results/health.d.ts +4 -0
  288. package/dist/results/health.js +27 -0
  289. package/dist/results/snapshot.js +15 -2
  290. package/dist/sanitizeReadinessDetail.js +51 -1
  291. package/dist/server/metadata.js +9 -2
  292. package/dist/server/normaliseContentRelationships.d.ts +7 -0
  293. package/dist/server/normaliseContentRelationships.js +62 -0
  294. package/dist/server/pageKindSeoContract.d.ts +27 -0
  295. package/dist/server/pageKindSeoContract.js +80 -0
  296. package/dist/server/recordContentChange.d.ts +1 -1
  297. package/dist/server/recordContentChange.js +72 -13
  298. package/dist/server/resolveExperimentBlocks.d.ts +5 -0
  299. package/dist/server/resolveExperimentBlocks.js +54 -0
  300. package/dist/server/runResults.js +92 -19
  301. package/dist/server/toClientAssignments.js +2 -1
  302. package/dist/transitions/addMetric.d.ts +2 -1
  303. package/dist/transitions/addMetric.js +28 -1
  304. package/dist/transitions/checkStart.d.ts +17 -1
  305. package/dist/transitions/checkStart.js +99 -7
  306. package/dist/transitions/decide.js +40 -1
  307. package/dist/transitions/deleteExperiment.d.ts +3 -1
  308. package/dist/transitions/deleteExperiment.js +27 -1
  309. package/dist/transitions/kernel.js +4 -0
  310. package/dist/transitions/makeContent.d.ts +5 -0
  311. package/dist/transitions/makeContent.js +39 -11
  312. package/dist/transitions/pageCollection.d.ts +10 -0
  313. package/dist/transitions/pageCollection.js +20 -0
  314. package/dist/transitions/pageVersions.d.ts +10 -0
  315. package/dist/transitions/pageVersions.js +44 -0
  316. package/dist/transitions/phaseRecord.d.ts +1 -0
  317. package/dist/transitions/placementInteractions.d.ts +40 -0
  318. package/dist/transitions/placementInteractions.js +94 -0
  319. package/dist/transitions/placements.d.ts +10 -6
  320. package/dist/transitions/placements.js +156 -2
  321. package/dist/transitions/preflightData.d.ts +8 -0
  322. package/dist/transitions/preflightData.js +248 -36
  323. package/dist/transitions/resolveCaps.d.ts +1 -0
  324. package/dist/transitions/resolveCaps.js +6 -2
  325. package/dist/transitions/start.js +77 -3
  326. package/dist/types/admin.d.ts +1 -1
  327. package/dist/types/assignments.d.ts +1 -0
  328. package/dist/types/contracts.d.ts +31 -10
  329. package/dist/types/endpoints.d.ts +92 -5
  330. package/dist/types/index.d.ts +3 -0
  331. package/dist/types/puck.d.ts +7 -1
  332. package/dist/types/snapshot.d.ts +17 -4
  333. package/dist/types/surface.d.ts +13 -0
  334. package/dist/ui/Tabs.d.ts +4 -2
  335. package/dist/ui/Tabs.js +33 -26
  336. package/dist/ui/accent.d.ts +22 -3
  337. package/dist/ui/accent.js +74 -16
  338. package/package.json +12 -2
@@ -0,0 +1,94 @@
1
+ import { placementPathContent, placementTests } from '../db/placements.js';
2
+ import { lockPagePaths } from '../db/locks.js';
3
+ import { normalisePath } from '../runtime/normalisePath.js';
4
+ import { scanBlockPlacements } from './placements.js';
5
+ import { interactingTestsCopy } from '../admin/copy/readiness.js';
6
+ import { XpRefusal } from '../errors.js';
7
+ import { pageTargetPaths } from './checkStart.js';
8
+ export async function detached(req, fn) {
9
+ const transactionID = req.transactionID;
10
+ delete req.transactionID;
11
+ try {
12
+ return await fn();
13
+ } finally{
14
+ req.transactionID = transactionID;
15
+ }
16
+ }
17
+ export function uniquePlacementDocs(placements) {
18
+ return [
19
+ ...new Map(placements.map((p)=>[
20
+ JSON.stringify([
21
+ p.collection,
22
+ String(p.id)
23
+ ]),
24
+ {
25
+ collection: p.collection,
26
+ id: p.id
27
+ }
28
+ ])).values()
29
+ ];
30
+ }
31
+ export function scanPlacements(req, options, slug) {
32
+ return scanBlockPlacements(req, {
33
+ slug,
34
+ puckCollections: options.puck?.collections ?? [],
35
+ blocksCollections: options.blocks?.collections ?? []
36
+ });
37
+ }
38
+ export async function placementPaths(req, options, docs) {
39
+ const paths = new Set();
40
+ for (const doc of docs){
41
+ const registration = options.pages?.collections.find((r)=>r.slug === doc.collection);
42
+ if (!registration) continue;
43
+ const row = await placementPathContent(req, doc.collection, doc.id);
44
+ if (!row || row._status === 'draft') continue;
45
+ const path = registration.pathOf(row);
46
+ if (path !== null) paths.add(normalisePath(path));
47
+ }
48
+ return [
49
+ ...paths
50
+ ].sort();
51
+ }
52
+ const active = (test)=>test.status === 'running' || test.status === 'decided';
53
+ export async function placementInteractions(req, exec, options, experiment, paths) {
54
+ const result = [];
55
+ for (const test of (await placementTests(exec))){
56
+ if (!active(test) || String(test.id) === String(experiment.id)) continue;
57
+ if (experiment.kind === 'block' && test.kind === 'page') {
58
+ for (const path of paths.filter((path)=>pageTargetPaths(test.arms).includes(path)))result.push({
59
+ experimentId: test.id,
60
+ slug: test.slug,
61
+ name: test.name,
62
+ status: test.status,
63
+ path,
64
+ version: test.arms.some((a)=>a.kind === 'variant' && a.pageTarget && normalisePath(a.pageTarget) === path)
65
+ });
66
+ } else if (experiment.kind === 'page' && test.kind === 'block') {
67
+ const docs = test.placementDocs ?? uniquePlacementDocs(await detached(req, ()=>scanPlacements(req, options, test.slug)));
68
+ const relevant = docs.filter((doc)=>!experiment.pageCollection || doc.collection === experiment.pageCollection);
69
+ for (const path of (await placementPaths(req, options, relevant)).filter((path)=>paths.includes(path)))result.push({
70
+ experimentId: test.id,
71
+ slug: test.slug,
72
+ name: test.name,
73
+ status: test.status,
74
+ path,
75
+ version: experiment.arms.some((a)=>a.kind === 'variant' && a.pageTarget && normalisePath(a.pageTarget) === path)
76
+ });
77
+ }
78
+ }
79
+ return result;
80
+ }
81
+ export async function lockAndRescanPlacements(req, exec, options, experiment, before) {
82
+ const paths = await placementPaths(req, options, uniquePlacementDocs(before));
83
+ await lockPagePaths(exec, paths);
84
+ const placements = await detached(req, ()=>scanPlacements(req, options, experiment.slug));
85
+ const docs = uniquePlacementDocs(placements);
86
+ const afterPaths = await placementPaths(req, options, docs);
87
+ if (afterPaths.some((path)=>!paths.includes(path))) throw new XpRefusal('placements-changed', "This test's pages changed while it was starting. Try again.", undefined, 409);
88
+ const conflicts = await placementInteractions(req, exec, options, experiment, paths);
89
+ if (conflicts.length) throw new XpRefusal('interacting-tests', interactingTestsCopy(conflicts[0].path, conflicts[0].status, conflicts[0].name, conflicts[0].version), conflicts, 409);
90
+ return {
91
+ placements,
92
+ docs
93
+ };
94
+ }
@@ -1,12 +1,12 @@
1
1
  import type { PayloadRequest } from 'payload';
2
- import type { Arm, DocId, FlagPoint, ResolvedOptions } from '../types/contracts.js';
2
+ import type { Arm, DocId, FlagPoint, PreviewPlacement, ResolvedOptions } from '../types/contracts.js';
3
3
  import type { ArmSlot } from '../types/puck.js';
4
- export interface BlockPlacement {
5
- collection: string;
6
- id: DocId;
7
- title: string;
8
- published: boolean;
4
+ export interface BlockPlacement extends PreviewPlacement {
5
+ editor?: 'puck' | 'blocks' | 'page';
6
+ doc?: Record<string, unknown>;
9
7
  slots: Record<ArmSlot, number>;
8
+ duplicateRows?: boolean;
9
+ identicalSlots?: string[];
10
10
  locked?: boolean;
11
11
  localized?: boolean;
12
12
  }
@@ -17,10 +17,14 @@ export type FlagPlacement = Awaited<ReturnType<NonNullable<FlagPoint['placements
17
17
  export declare function scanBlockPlacements(req: PayloadRequest, args: {
18
18
  slug: string;
19
19
  puckCollections: NonNullable<ResolvedOptions['puck']>['collections'];
20
+ blocksCollections: NonNullable<ResolvedOptions['blocks']>['collections'];
21
+ compareVersions?: boolean;
20
22
  }): Promise<BlockPlacement[]>;
21
23
  export declare function scanBlockPlacements(req: PayloadRequest, args: {
22
24
  slugs: ReadonlySet<string>;
23
25
  puckCollections: NonNullable<ResolvedOptions['puck']>['collections'];
26
+ blocksCollections: NonNullable<ResolvedOptions['blocks']>['collections'];
27
+ compareVersions?: boolean;
24
28
  }): Promise<Map<string, BlockPlacement[]>>;
25
29
  export declare function flagPlacements<TUser extends {
26
30
  id: DocId;
@@ -1,5 +1,10 @@
1
+ import { BLOCK_SLOTS, EXPERIMENT_BLOCK_SLUG, hostBlocks, slotBlocks } from '../blocks/model.js';
2
+ import { safeError } from '../log.js';
3
+ import { pageVersionContent } from '../db/pageContent.js';
4
+ import { comparableContent } from './pageVersions.js';
5
+ import { isDeepStrictEqual } from 'node:util';
1
6
  import { hasLocalizedFields } from './contentFields.js';
2
- import { puckCollections as normalisePuckCollections } from '../options.js';
7
+ import { documentEditor, puckCollections as normalisePuckCollections } from '../options.js';
3
8
  const slots = [
4
9
  'control',
5
10
  'variantA',
@@ -23,7 +28,9 @@ export async function scanBlockPlacements(req, args) {
23
28
  collections: puckCollections
24
29
  }
25
30
  })){
31
+ const editors = new Map();
26
32
  const publishedIds = new Set();
33
+ const publishedSlugs = new Map();
27
34
  const draftsEnabled = req.payload.config.collections?.some((config)=>config.slug === collection && config.versions && config.versions.drafts);
28
35
  for (const draft of [
29
36
  false,
@@ -51,7 +58,29 @@ export async function scanBlockPlacements(req, args) {
51
58
  for (const doc of result.docs){
52
59
  if (!draft && doc._status === 'draft') continue;
53
60
  if (draft && doc._status !== 'draft' && publishedIds.has(String(doc.id))) continue;
54
- if (!draft) publishedIds.add(String(doc.id));
61
+ if (!draft) {
62
+ editors.set(String(doc.id), documentEditor({
63
+ puck: {
64
+ enabled: true,
65
+ collections: puckCollections
66
+ },
67
+ blocks: {
68
+ collections: args.blocksCollections
69
+ }
70
+ }, collection, doc));
71
+ publishedIds.add(String(doc.id));
72
+ if (typeof doc.slug === 'string') publishedSlugs.set(String(doc.id), doc.slug);
73
+ }
74
+ const editor = editors.get(String(doc.id)) ?? documentEditor({
75
+ puck: {
76
+ enabled: true,
77
+ collections: puckCollections
78
+ },
79
+ blocks: {
80
+ collections: args.blocksCollections
81
+ }
82
+ }, collection, doc);
83
+ if (editor !== 'puck') continue;
55
84
  function walk(value) {
56
85
  if (!value || typeof value !== 'object') return;
57
86
  if (Array.isArray(value)) {
@@ -66,8 +95,13 @@ export async function scanBlockPlacements(req, args) {
66
95
  placements.get(experiment).push({
67
96
  collection,
68
97
  id: doc.id,
98
+ editor: 'puck',
99
+ doc,
69
100
  title: String(doc.title ?? doc.name ?? doc.id),
70
101
  published: !draft || doc._status === 'published',
102
+ ...publishedSlugs.has(String(doc.id)) ? {
103
+ slug: publishedSlugs.get(String(doc.id))
104
+ } : {},
71
105
  slots: Object.fromEntries(slots.map((slot)=>[
72
106
  slot,
73
107
  Array.isArray(props[slot]) ? props[slot].length : 0
@@ -80,7 +114,115 @@ export async function scanBlockPlacements(req, args) {
80
114
  }
81
115
  Object.values(node).forEach(walk);
82
116
  }
117
+ const offsets = new Map([
118
+ ...placements
119
+ ].map(([slug, list])=>[
120
+ slug,
121
+ list.length
122
+ ]));
83
123
  walk(doc[field]);
124
+ if (args.compareVersions && !draft && [
125
+ ...placements
126
+ ].some(([slug, list])=>list.length > offsets.get(slug))) {
127
+ const raw = await comparisonContent(req, collection, doc.id);
128
+ const rawSlots = new Map();
129
+ const collect = (value)=>{
130
+ if (Array.isArray(value)) {
131
+ value.forEach(collect);
132
+ return;
133
+ }
134
+ if (!value || typeof value !== 'object') return;
135
+ const node = value, props = node.props;
136
+ const ref = props?.experiment;
137
+ const slug = typeof ref === 'string' ? ref : ref && typeof ref === 'object' && 'slug' in ref ? ref.slug : undefined;
138
+ if (node.type === 'ExperimentSlot' && props && typeof slug === 'string') {
139
+ const list = rawSlots.get(slug) ?? [];
140
+ list.push(props);
141
+ rawSlots.set(slug, list);
142
+ }
143
+ Object.values(node).forEach(collect);
144
+ };
145
+ if (raw && raw._status !== 'draft') collect(raw[field]);
146
+ for (const [slug, list] of placements)list.slice(offsets.get(slug)).forEach((placement, index)=>{
147
+ const props = rawSlots.get(slug)?.[index];
148
+ if (props) placement.identicalSlots = slots.filter((slot)=>slot !== 'control' && Array.isArray(props[slot]) && props[slot].length > 0 && isDeepStrictEqual(comparableContent(props.control ?? []), comparableContent(props[slot])));
149
+ });
150
+ }
151
+ }
152
+ if (!result.hasNextPage) break;
153
+ page++;
154
+ }
155
+ }
156
+ }
157
+ for (const registration of args.blocksCollections){
158
+ const { slug: collection, field } = registration;
159
+ const published = new Map();
160
+ const draftsEnabled = req.payload.config.collections?.some((c)=>c.slug === collection && c.versions && c.versions.drafts);
161
+ for (const draft of [
162
+ false,
163
+ true
164
+ ]){
165
+ let page = 1;
166
+ while(true){
167
+ const result = await req.payload.find({
168
+ collection,
169
+ req,
170
+ overrideAccess: false,
171
+ depth: 0,
172
+ limit: 100,
173
+ page,
174
+ draft,
175
+ includeLockStatus: true,
176
+ ...!draft && draftsEnabled ? {
177
+ where: {
178
+ _status: {
179
+ equals: 'published'
180
+ }
181
+ }
182
+ } : {}
183
+ });
184
+ for (const doc of result.docs){
185
+ if (!draft && doc._status === 'draft') continue;
186
+ if (!draft) published.set(String(doc.id), doc);
187
+ else if (doc._status !== 'draft' && published.has(String(doc.id))) continue;
188
+ const baseline = published.get(String(doc.id)) ?? doc;
189
+ if (documentEditor({
190
+ blocks: {
191
+ collections: args.blocksCollections
192
+ },
193
+ puck: {
194
+ enabled: true,
195
+ collections: puckCollections
196
+ }
197
+ }, collection, baseline) !== 'blocks') continue;
198
+ const raw = args.compareVersions && !draft && hostBlocks(doc[field]).some((block)=>block.blockType === (registration.blockSlug ?? EXPERIMENT_BLOCK_SLUG) && typeof block.experiment === 'string' && slugs.has(block.experiment)) ? await comparisonContent(req, collection, doc.id) : undefined;
199
+ for (const [index, block] of hostBlocks(doc[field]).entries()){
200
+ if (block.blockType !== (registration.blockSlug ?? EXPERIMENT_BLOCK_SLUG) || typeof block.experiment !== 'string' || !slugs.has(block.experiment)) continue;
201
+ const stored = raw ? hostBlocks(raw[field]).find((row)=>row.id === block.id) ?? hostBlocks(raw[field])[index] ?? block : block;
202
+ const rows = Array.isArray(stored.slots) ? stored.slots : [];
203
+ const keys = rows.map((row)=>row?.slot);
204
+ placements.get(block.experiment).push({
205
+ collection,
206
+ id: doc.id,
207
+ editor: 'blocks',
208
+ doc,
209
+ title: String(doc.title ?? doc.name ?? doc.id),
210
+ published: !draft || doc._status === 'published',
211
+ ...typeof baseline.slug === 'string' ? {
212
+ slug: baseline.slug
213
+ } : {},
214
+ slots: Object.fromEntries(BLOCK_SLOTS.map((slot)=>[
215
+ slot,
216
+ slotBlocks(block, slot).length
217
+ ])),
218
+ duplicateRows: new Set(keys).size !== keys.length,
219
+ identicalSlots: raw ? BLOCK_SLOTS.filter((slot)=>slot !== 'control' && slotBlocks(block, slot).length > 0 && isDeepStrictEqual(comparableContent(slotBlocks(stored, 'control')), comparableContent(slotBlocks(stored, slot)))) : [],
220
+ locked: doc._isLocked === true || doc.locked === true,
221
+ ...hasLocalizedFields(req.payload.config.collections?.find((c)=>c.slug === collection)?.fields ?? [], req.payload.config) ? {
222
+ localized: true
223
+ } : {}
224
+ });
225
+ }
84
226
  }
85
227
  if (!result.hasNextPage) break;
86
228
  page++;
@@ -98,3 +240,15 @@ export async function flagPlacements(options, experiment, req) {
98
240
  req
99
241
  });
100
242
  }
243
+ /** A warning-only comparison must never turn an adapter failure into a Start refusal. */ async function comparisonContent(req, collection, id) {
244
+ try {
245
+ return await pageVersionContent(req, collection, id);
246
+ } catch (cause) {
247
+ req.payload.logger.error({
248
+ err: safeError(cause),
249
+ collection,
250
+ id
251
+ }, 'Experiment version comparison failed');
252
+ return undefined;
253
+ }
254
+ }
@@ -13,6 +13,14 @@ 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
+ /** Occupancy from the arms the runtime routes on (frozen rounds), shared by preflight and
17
+ * Start's locked recheck. Raw SQL on the request's transaction when there is one. */
18
+ export declare function loadPageTestsInUse(req: PayloadRequest): Promise<NonNullable<CheckStartInput['pageTestsInUse']>>;
19
+ /** Page targets no registered collection reports as published. A lookup that throws counts as
20
+ * unpublished (logged) unless propagateErrors is requested by decide. Start calls it again under its page-path locks on a detached request. */
21
+ export declare function unpublishedPageTargets<TUser extends {
22
+ id: DocId;
23
+ }>(req: PayloadRequest, options: ResolvedOptions<TUser>, experiment: Pick<StartExperiment, 'id' | 'arms' | 'pageCollection'>, propagateErrors?: boolean): Promise<string[]>;
16
24
  /** Shared, request-scoped reads for summary; a failed row is null in tolerant mode.
17
25
  * Preflight/Start use the same builder and retain their throwing read behaviour.
18
26
  */