@delmaredigital/payload-experiments 0.0.0-stage → 0.1.0-rc.6

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 (466) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +590 -2
  3. package/dist/access.d.ts +1 -0
  4. package/dist/access.js +2 -0
  5. package/dist/admin/AllocationField.d.ts +2 -0
  6. package/dist/admin/AllocationField.js +80 -0
  7. package/dist/admin/FlagPointSelect.d.ts +2 -0
  8. package/dist/admin/FlagPointSelect.js +54 -0
  9. package/dist/admin/LayerUsage.d.ts +2 -0
  10. package/dist/admin/LayerUsage.js +24 -0
  11. package/dist/admin/LockedFieldNotice.d.ts +2 -0
  12. package/dist/admin/LockedFieldNotice.js +26 -0
  13. package/dist/admin/MetricSourceSelect.d.ts +2 -0
  14. package/dist/admin/MetricSourceSelect.js +32 -0
  15. package/dist/admin/PhaseHistory.d.ts +2 -0
  16. package/dist/admin/PhaseHistory.js +76 -0
  17. package/dist/admin/PreviewLinks.d.ts +11 -0
  18. package/dist/admin/PreviewLinks.js +69 -0
  19. package/dist/admin/StatusCell.d.ts +3 -0
  20. package/dist/admin/StatusCell.js +19 -0
  21. package/dist/admin/SummaryCell.d.ts +2 -0
  22. package/dist/admin/SummaryCell.js +32 -0
  23. package/dist/admin/actions/AddMetricDialog.d.ts +6 -0
  24. package/dist/admin/actions/AddMetricDialog.js +138 -0
  25. package/dist/admin/actions/AllocateDialog.d.ts +6 -0
  26. package/dist/admin/actions/AllocateDialog.js +81 -0
  27. package/dist/admin/actions/ConfirmDialog.d.ts +14 -0
  28. package/dist/admin/actions/ConfirmDialog.js +192 -0
  29. package/dist/admin/actions/DecideDialog.d.ts +7 -0
  30. package/dist/admin/actions/DecideDialog.js +173 -0
  31. package/dist/admin/actions/ExperimentActions.d.ts +3 -0
  32. package/dist/admin/actions/ExperimentActions.js +110 -0
  33. package/dist/admin/actions/MakeContentResult.d.ts +6 -0
  34. package/dist/admin/actions/MakeContentResult.js +50 -0
  35. package/dist/admin/actions/actions.model.d.ts +37 -0
  36. package/dist/admin/actions/actions.model.js +71 -0
  37. package/dist/admin/actions/decide.model.d.ts +21 -0
  38. package/dist/admin/actions/decide.model.js +38 -0
  39. package/dist/admin/allocation.model.d.ts +20 -0
  40. package/dist/admin/allocation.model.js +38 -0
  41. package/dist/admin/api.d.ts +32 -0
  42. package/dist/admin/api.js +95 -0
  43. package/dist/admin/armSelect/ArmSelectField.d.ts +2 -0
  44. package/dist/admin/armSelect/ArmSelectField.js +185 -0
  45. package/dist/admin/armSelect/armSelect.model.d.ts +30 -0
  46. package/dist/admin/armSelect/armSelect.model.js +20 -0
  47. package/dist/admin/arms/ArmsField.d.ts +2 -0
  48. package/dist/admin/arms/ArmsField.js +310 -0
  49. package/dist/admin/arms/ParamsEditor.d.ts +16 -0
  50. package/dist/admin/arms/ParamsEditor.js +79 -0
  51. package/dist/admin/arms/arms.model.d.ts +21 -0
  52. package/dist/admin/arms/arms.model.js +153 -0
  53. package/dist/admin/arms/pageTarget.model.d.ts +4 -0
  54. package/dist/admin/arms/pageTarget.model.js +19 -0
  55. package/dist/admin/arms/params.model.d.ts +7 -0
  56. package/dist/admin/arms/params.model.js +35 -0
  57. package/dist/admin/copy/actions.d.ts +38 -0
  58. package/dist/admin/copy/actions.js +38 -0
  59. package/dist/admin/copy/armSelect.d.ts +11 -0
  60. package/dist/admin/copy/armSelect.js +11 -0
  61. package/dist/admin/copy/arms.d.ts +37 -0
  62. package/dist/admin/copy/arms.js +47 -0
  63. package/dist/admin/copy/common.d.ts +20 -0
  64. package/dist/admin/copy/common.js +27 -0
  65. package/dist/admin/copy/history.d.ts +9 -0
  66. package/dist/admin/copy/history.js +9 -0
  67. package/dist/admin/copy/metrics.d.ts +9 -0
  68. package/dist/admin/copy/metrics.js +9 -0
  69. package/dist/admin/copy/results.d.ts +78 -0
  70. package/dist/admin/copy/results.js +81 -0
  71. package/dist/admin/copy/review.d.ts +33 -0
  72. package/dist/admin/copy/review.js +127 -0
  73. package/dist/admin/copy/status.d.ts +8 -0
  74. package/dist/admin/copy/status.js +8 -0
  75. package/dist/admin/copy/tabs.d.ts +7 -0
  76. package/dist/admin/copy/tabs.js +7 -0
  77. package/dist/admin/copy/who.d.ts +36 -0
  78. package/dist/admin/copy/who.js +36 -0
  79. package/dist/admin/labels.d.ts +45 -0
  80. package/dist/admin/labels.js +53 -0
  81. package/dist/admin/layerUsage.model.d.ts +15 -0
  82. package/dist/admin/layerUsage.model.js +18 -0
  83. package/dist/admin/lockedNotice.model.d.ts +3 -0
  84. package/dist/admin/lockedNotice.model.js +18 -0
  85. package/dist/admin/metricSource.model.d.ts +7 -0
  86. package/dist/admin/metricSource.model.js +11 -0
  87. package/dist/admin/paths.d.ts +16 -0
  88. package/dist/admin/paths.js +16 -0
  89. package/dist/admin/permissions.model.d.ts +10 -0
  90. package/dist/admin/permissions.model.js +4 -0
  91. package/dist/admin/phaseHistory.model.d.ts +19 -0
  92. package/dist/admin/phaseHistory.model.js +19 -0
  93. package/dist/admin/reload.d.ts +1 -0
  94. package/dist/admin/reload.js +1 -0
  95. package/dist/admin/results/ArmTable.d.ts +8 -0
  96. package/dist/admin/results/ArmTable.js +112 -0
  97. package/dist/admin/results/CappedCell.d.ts +4 -0
  98. package/dist/admin/results/CappedCell.js +24 -0
  99. package/dist/admin/results/CiChart.d.ts +10 -0
  100. package/dist/admin/results/CiChart.js +178 -0
  101. package/dist/admin/results/DataQualityPanel.d.ts +4 -0
  102. package/dist/admin/results/DataQualityPanel.js +40 -0
  103. package/dist/admin/results/ResultsView.d.ts +3 -0
  104. package/dist/admin/results/ResultsView.js +312 -0
  105. package/dist/admin/results/VerdictCard.d.ts +9 -0
  106. package/dist/admin/results/VerdictCard.js +49 -0
  107. package/dist/admin/results/chart.model.d.ts +32 -0
  108. package/dist/admin/results/chart.model.js +52 -0
  109. package/dist/admin/results/format.d.ts +10 -0
  110. package/dist/admin/results/format.js +55 -0
  111. package/dist/admin/results/results.model.d.ts +105 -0
  112. package/dist/admin/results/results.model.js +175 -0
  113. package/dist/admin/review/ReviewPanel.d.ts +2 -0
  114. package/dist/admin/review/ReviewPanel.js +268 -0
  115. package/dist/admin/review/review.model.d.ts +31 -0
  116. package/dist/admin/review/review.model.js +40 -0
  117. package/dist/admin/status.model.d.ts +16 -0
  118. package/dist/admin/status.model.js +17 -0
  119. package/dist/admin/styles.css +10 -0
  120. package/dist/admin/styles.d.ts +1 -0
  121. package/dist/admin/styles.js +2 -0
  122. package/dist/admin/summaryBatcher.d.ts +8 -0
  123. package/dist/admin/summaryBatcher.js +33 -0
  124. package/dist/admin/targeting/TargetingAttributeField.d.ts +2 -0
  125. package/dist/admin/targeting/TargetingAttributeField.js +31 -0
  126. package/dist/admin/targeting/TargetingOpField.d.ts +2 -0
  127. package/dist/admin/targeting/TargetingOpField.js +45 -0
  128. package/dist/admin/targeting/TargetingValueField.d.ts +2 -0
  129. package/dist/admin/targeting/TargetingValueField.js +142 -0
  130. package/dist/admin/targeting/targeting.model.d.ts +18 -0
  131. package/dist/admin/targeting/targeting.model.js +48 -0
  132. package/dist/admin/useApi.d.ts +20 -0
  133. package/dist/admin/useApi.js +10 -0
  134. package/dist/admin/useLayerUsage.d.ts +15 -0
  135. package/dist/admin/useLayerUsage.js +92 -0
  136. package/dist/client/ExperimentPageview.d.ts +1 -0
  137. package/dist/client/ExperimentPageview.js +27 -0
  138. package/dist/client/ExperimentsProvider.d.ts +6 -0
  139. package/dist/client/ExperimentsProvider.js +22 -0
  140. package/dist/client/ViewportExposure.d.ts +9 -0
  141. package/dist/client/ViewportExposure.js +69 -0
  142. package/dist/client/beacon.d.ts +17 -0
  143. package/dist/client/beacon.js +71 -0
  144. package/dist/client/context.d.ts +2 -0
  145. package/dist/client/context.js +2 -0
  146. package/dist/client/idempotency.d.ts +2 -0
  147. package/dist/client/idempotency.js +13 -0
  148. package/dist/client/trackConversionBeacon.d.ts +3 -0
  149. package/dist/client/trackConversionBeacon.js +16 -0
  150. package/dist/client/useExperiment.d.ts +5 -0
  151. package/dist/client/useExperiment.js +27 -0
  152. package/dist/client/useFlagPoint.d.ts +9 -0
  153. package/dist/client/useFlagPoint.js +32 -0
  154. package/dist/collections/access.d.ts +21 -0
  155. package/dist/collections/access.js +25 -0
  156. package/dist/collections/events.d.ts +3 -0
  157. package/dist/collections/events.js +117 -0
  158. package/dist/collections/experiments/armInvariants.d.ts +12 -0
  159. package/dist/collections/experiments/armInvariants.js +89 -0
  160. package/dist/collections/experiments/config.d.ts +3 -0
  161. package/dist/collections/experiments/config.js +574 -0
  162. package/dist/collections/experiments/createDefaults.d.ts +2 -0
  163. package/dist/collections/experiments/createDefaults.js +7 -0
  164. package/dist/collections/experiments/deleteGuard.d.ts +3 -0
  165. package/dist/collections/experiments/deleteGuard.js +8 -0
  166. package/dist/collections/experiments/editability.d.ts +4 -0
  167. package/dist/collections/experiments/editability.js +35 -0
  168. package/dist/collections/experiments/flagControlParams.d.ts +2 -0
  169. package/dist/collections/experiments/flagControlParams.js +32 -0
  170. package/dist/collections/experiments/lockHook.d.ts +2 -0
  171. package/dist/collections/experiments/lockHook.js +134 -0
  172. package/dist/collections/identities.d.ts +3 -0
  173. package/dist/collections/identities.js +30 -0
  174. package/dist/collections/index.d.ts +6 -0
  175. package/dist/collections/index.js +24 -0
  176. package/dist/collections/layers.d.ts +3 -0
  177. package/dist/collections/layers.js +77 -0
  178. package/dist/collections/metrics.d.ts +3 -0
  179. package/dist/collections/metrics.js +115 -0
  180. package/dist/collections/phases.d.ts +3 -0
  181. package/dist/collections/phases.js +59 -0
  182. package/dist/collections/serialize.d.ts +5 -0
  183. package/dist/collections/serialize.js +40 -0
  184. package/dist/collections/snapshots.d.ts +3 -0
  185. package/dist/collections/snapshots.js +47 -0
  186. package/dist/constants.d.ts +7 -0
  187. package/dist/constants.js +7 -0
  188. package/dist/db/adapter.d.ts +3 -0
  189. package/dist/db/adapter.js +9 -0
  190. package/dist/db/appends.d.ts +58 -0
  191. package/dist/db/appends.js +116 -0
  192. package/dist/db/delete.d.ts +17 -0
  193. package/dist/db/delete.js +35 -0
  194. package/dist/db/erase.d.ts +12 -0
  195. package/dist/db/erase.js +55 -0
  196. package/dist/db/events.d.ts +21 -0
  197. package/dist/db/events.js +100 -0
  198. package/dist/db/executor.d.ts +10 -0
  199. package/dist/db/executor.js +37 -0
  200. package/dist/db/identities.d.ts +11 -0
  201. package/dist/db/identities.js +29 -0
  202. package/dist/db/locks.d.ts +15 -0
  203. package/dist/db/locks.js +23 -0
  204. package/dist/db/manifest.d.ts +8 -0
  205. package/dist/db/manifest.js +8 -0
  206. package/dist/db/names.d.ts +271 -0
  207. package/dist/db/names.js +270 -0
  208. package/dist/db/pgExecutor.d.ts +41 -0
  209. package/dist/db/pgExecutor.js +250 -0
  210. package/dist/db/results.d.ts +53 -0
  211. package/dist/db/results.js +103 -0
  212. package/dist/db/slices.d.ts +18 -0
  213. package/dist/db/slices.js +51 -0
  214. package/dist/db/snapshots.d.ts +47 -0
  215. package/dist/db/snapshots.js +89 -0
  216. package/dist/db/sql.d.ts +5 -0
  217. package/dist/db/sql.js +5 -0
  218. package/dist/db/state.d.ts +12 -0
  219. package/dist/db/state.js +27 -0
  220. package/dist/db/sweep.d.ts +13 -0
  221. package/dist/db/sweep.js +90 -0
  222. package/dist/db/txid.d.ts +7 -0
  223. package/dist/db/txid.js +12 -0
  224. package/dist/define.d.ts +3 -0
  225. package/dist/define.js +6 -0
  226. package/dist/endpoints/events.d.ts +7 -0
  227. package/dist/endpoints/events.js +98 -0
  228. package/dist/endpoints/index.d.ts +18 -0
  229. package/dist/endpoints/index.js +160 -0
  230. package/dist/endpoints/manifest.d.ts +7 -0
  231. package/dist/endpoints/manifest.js +30 -0
  232. package/dist/endpoints/preflight.d.ts +8 -0
  233. package/dist/endpoints/preflight.js +25 -0
  234. package/dist/endpoints/previewToken.d.ts +7 -0
  235. package/dist/endpoints/previewToken.js +54 -0
  236. package/dist/endpoints/recompute.d.ts +7 -0
  237. package/dist/endpoints/recompute.js +41 -0
  238. package/dist/endpoints/results.d.ts +24 -0
  239. package/dist/endpoints/results.js +136 -0
  240. package/dist/endpoints/summary.d.ts +7 -0
  241. package/dist/endpoints/summary.js +40 -0
  242. package/dist/endpoints/transitions.d.ts +42 -0
  243. package/dist/endpoints/transitions.js +154 -0
  244. package/dist/errors.d.ts +27 -0
  245. package/dist/errors.js +33 -0
  246. package/dist/events/body.d.ts +7 -0
  247. package/dist/events/body.js +43 -0
  248. package/dist/events/dimensions.d.ts +4 -0
  249. package/dist/events/dimensions.js +51 -0
  250. package/dist/events/preview.d.ts +2 -0
  251. package/dist/events/preview.js +9 -0
  252. package/dist/events/request.d.ts +3 -0
  253. package/dist/events/request.js +14 -0
  254. package/dist/events/rows.d.ts +7 -0
  255. package/dist/events/rows.js +65 -0
  256. package/dist/exports/adminClient.d.ts +16 -0
  257. package/dist/exports/adminClient.js +17 -0
  258. package/dist/exports/client.d.ts +7 -0
  259. package/dist/exports/client.js +7 -0
  260. package/dist/exports/index.d.ts +4 -0
  261. package/dist/exports/index.js +3 -0
  262. package/dist/exports/proxy.d.ts +4 -0
  263. package/dist/exports/proxy.js +3 -0
  264. package/dist/exports/puck.d.ts +4 -0
  265. package/dist/exports/puck.js +3 -0
  266. package/dist/exports/puckEditor.d.ts +1 -0
  267. package/dist/exports/puckEditor.js +2 -0
  268. package/dist/exports/rsc.d.ts +13 -0
  269. package/dist/exports/rsc.js +13 -0
  270. package/dist/exports/runtimeProxy.d.ts +4 -0
  271. package/dist/exports/runtimeProxy.js +2 -0
  272. package/dist/exports/runtimeServer.d.ts +2 -0
  273. package/dist/exports/runtimeServer.js +1 -0
  274. package/dist/fields/armSelect.d.ts +7 -0
  275. package/dist/fields/armSelect.js +33 -0
  276. package/dist/globals/state.d.ts +3 -0
  277. package/dist/globals/state.js +31 -0
  278. package/dist/hooks/contentChanges.d.ts +3 -0
  279. package/dist/hooks/contentChanges.js +10 -0
  280. package/dist/hooks/hostHook.d.ts +5 -0
  281. package/dist/hooks/hostHook.js +20 -0
  282. package/dist/index.d.ts +1 -0
  283. package/dist/index.js +4 -0
  284. package/dist/manifest/cache.d.ts +10 -0
  285. package/dist/manifest/cache.js +51 -0
  286. package/dist/manifest/compile.d.ts +63 -0
  287. package/dist/manifest/compile.js +237 -0
  288. package/dist/onInit.d.ts +5 -0
  289. package/dist/onInit.js +13 -0
  290. package/dist/options/fromReq.d.ts +3 -0
  291. package/dist/options/fromReq.js +5 -0
  292. package/dist/options.d.ts +9 -0
  293. package/dist/options.js +107 -0
  294. package/dist/plugin.d.ts +7 -0
  295. package/dist/plugin.js +62 -0
  296. package/dist/proxy/apply.d.ts +3 -0
  297. package/dist/proxy/apply.js +5 -0
  298. package/dist/proxy/consentFallback.d.ts +5 -0
  299. package/dist/proxy/consentFallback.js +27 -0
  300. package/dist/proxy/control.d.ts +6 -0
  301. package/dist/proxy/control.js +36 -0
  302. package/dist/proxy/createProxy.d.ts +3 -0
  303. package/dist/proxy/createProxy.js +15 -0
  304. package/dist/proxy/pageRewrite.d.ts +4 -0
  305. package/dist/proxy/pageRewrite.js +41 -0
  306. package/dist/proxy/resolve.d.ts +13 -0
  307. package/dist/proxy/resolve.js +163 -0
  308. package/dist/proxy/skip.d.ts +1 -0
  309. package/dist/proxy/skip.js +9 -0
  310. package/dist/puck/ExperimentPicker.d.ts +5 -0
  311. package/dist/puck/ExperimentPicker.js +88 -0
  312. package/dist/puck/ExperimentSlot.d.ts +3 -0
  313. package/dist/puck/ExperimentSlot.js +16 -0
  314. package/dist/puck/copy.d.ts +14 -0
  315. package/dist/puck/copy.js +14 -0
  316. package/dist/puck/editor/CreateExperimentDrawer.d.ts +5 -0
  317. package/dist/puck/editor/CreateExperimentDrawer.js +120 -0
  318. package/dist/puck/editor/ExperimentSlotEditor.d.ts +3 -0
  319. package/dist/puck/editor/ExperimentSlotEditor.js +225 -0
  320. package/dist/puck/editor/cloneSlot.d.ts +3 -0
  321. package/dist/puck/editor/cloneSlot.js +41 -0
  322. package/dist/puck/editor/copy.d.ts +25 -0
  323. package/dist/puck/editor/copy.js +25 -0
  324. package/dist/puck/editor/editor.model.d.ts +16 -0
  325. package/dist/puck/editor/editor.model.js +33 -0
  326. package/dist/puck/experimentSlotField.d.ts +5 -0
  327. package/dist/puck/experimentSlotField.js +13 -0
  328. package/dist/puck/slot.model.d.ts +2 -0
  329. package/dist/puck/slot.model.js +11 -0
  330. package/dist/puck/slotConfig.d.ts +5 -0
  331. package/dist/puck/slotConfig.js +25 -0
  332. package/dist/results/aggregate.d.ts +34 -0
  333. package/dist/results/aggregate.js +66 -0
  334. package/dist/results/analyse.d.ts +79 -0
  335. package/dist/results/analyse.js +378 -0
  336. package/dist/results/cuped.d.ts +16 -0
  337. package/dist/results/cuped.js +31 -0
  338. package/dist/results/decideGuard.d.ts +3 -0
  339. package/dist/results/decideGuard.js +8 -0
  340. package/dist/results/guardrails.d.ts +17 -0
  341. package/dist/results/guardrails.js +10 -0
  342. package/dist/results/history.d.ts +13 -0
  343. package/dist/results/history.js +39 -0
  344. package/dist/results/identityCredit.d.ts +16 -0
  345. package/dist/results/identityCredit.js +46 -0
  346. package/dist/results/power.d.ts +15 -0
  347. package/dist/results/power.js +60 -0
  348. package/dist/results/rolloutUnits.d.ts +17 -0
  349. package/dist/results/rolloutUnits.js +26 -0
  350. package/dist/results/snapshot.d.ts +9 -0
  351. package/dist/results/snapshot.js +154 -0
  352. package/dist/results/srm.d.ts +9 -0
  353. package/dist/results/srm.js +22 -0
  354. package/dist/results/verdict.d.ts +82 -0
  355. package/dist/results/verdict.js +85 -0
  356. package/dist/runtime/assign.d.ts +23 -0
  357. package/dist/runtime/assign.js +104 -0
  358. package/dist/runtime/bot.d.ts +1 -0
  359. package/dist/runtime/bot.js +7 -0
  360. package/dist/runtime/cookie.d.ts +17 -0
  361. package/dist/runtime/cookie.js +35 -0
  362. package/dist/runtime/eligibility.d.ts +22 -0
  363. package/dist/runtime/eligibility.js +20 -0
  364. package/dist/runtime/header.d.ts +17 -0
  365. package/dist/runtime/header.js +97 -0
  366. package/dist/runtime/loaders.d.ts +17 -0
  367. package/dist/runtime/loaders.js +29 -0
  368. package/dist/runtime/normalisePath.d.ts +7 -0
  369. package/dist/runtime/normalisePath.js +15 -0
  370. package/dist/runtime/previewToken.d.ts +9 -0
  371. package/dist/runtime/previewToken.js +23 -0
  372. package/dist/runtime/proxyRuntime.d.ts +2 -0
  373. package/dist/runtime/proxyRuntime.js +22 -0
  374. package/dist/runtime/seed.d.ts +2 -0
  375. package/dist/runtime/seed.js +4 -0
  376. package/dist/runtime/signing.d.ts +3 -0
  377. package/dist/runtime/signing.js +16 -0
  378. package/dist/runtime/targeting.d.ts +6 -0
  379. package/dist/runtime/targeting.js +9 -0
  380. package/dist/server/context.d.ts +10 -0
  381. package/dist/server/context.js +59 -0
  382. package/dist/server/eraseUser.d.ts +7 -0
  383. package/dist/server/eraseUser.js +4 -0
  384. package/dist/server/getAssignments.d.ts +9 -0
  385. package/dist/server/getAssignments.js +227 -0
  386. package/dist/server/getFlagPoint.d.ts +11 -0
  387. package/dist/server/getFlagPoint.js +36 -0
  388. package/dist/server/headerRevision.d.ts +6 -0
  389. package/dist/server/headerRevision.js +10 -0
  390. package/dist/server/linkIdentity.d.ts +9 -0
  391. package/dist/server/linkIdentity.js +34 -0
  392. package/dist/server/metadata.d.ts +7 -0
  393. package/dist/server/metadata.js +24 -0
  394. package/dist/server/previewDraft.d.ts +7 -0
  395. package/dist/server/previewDraft.js +51 -0
  396. package/dist/server/recordContentChange.d.ts +7 -0
  397. package/dist/server/recordContentChange.js +150 -0
  398. package/dist/server/recordExposures.d.ts +7 -0
  399. package/dist/server/recordExposures.js +56 -0
  400. package/dist/server/request.d.ts +7 -0
  401. package/dist/server/request.js +39 -0
  402. package/dist/server/resolveExperimentData.d.ts +6 -0
  403. package/dist/server/resolveExperimentData.js +64 -0
  404. package/dist/server/runResults.d.ts +22 -0
  405. package/dist/server/runResults.js +307 -0
  406. package/dist/server/serverRuntime.d.ts +7 -0
  407. package/dist/server/serverRuntime.js +20 -0
  408. package/dist/server/sitemap.d.ts +3 -0
  409. package/dist/server/sitemap.js +6 -0
  410. package/dist/server/toClientAssignments.d.ts +7 -0
  411. package/dist/server/toClientAssignments.js +56 -0
  412. package/dist/server/trackConversion.d.ts +12 -0
  413. package/dist/server/trackConversion.js +69 -0
  414. package/dist/targeting/ops.d.ts +8 -0
  415. package/dist/targeting/ops.js +58 -0
  416. package/dist/transitions/addMetric.d.ts +16 -0
  417. package/dist/transitions/addMetric.js +126 -0
  418. package/dist/transitions/allocate.d.ts +9 -0
  419. package/dist/transitions/allocate.js +84 -0
  420. package/dist/transitions/archive.d.ts +9 -0
  421. package/dist/transitions/archive.js +46 -0
  422. package/dist/transitions/checkStart.d.ts +61 -0
  423. package/dist/transitions/checkStart.js +104 -0
  424. package/dist/transitions/confirmPhrases.d.ts +5 -0
  425. package/dist/transitions/confirmPhrases.js +18 -0
  426. package/dist/transitions/decide.d.ts +9 -0
  427. package/dist/transitions/decide.js +138 -0
  428. package/dist/transitions/deleteExperiment.d.ts +5 -0
  429. package/dist/transitions/deleteExperiment.js +66 -0
  430. package/dist/transitions/kernel.d.ts +31 -0
  431. package/dist/transitions/kernel.js +69 -0
  432. package/dist/transitions/makeContent.d.ts +20 -0
  433. package/dist/transitions/makeContent.js +411 -0
  434. package/dist/transitions/phaseRecord.d.ts +27 -0
  435. package/dist/transitions/phaseRecord.js +81 -0
  436. package/dist/transitions/placements.d.ts +26 -0
  437. package/dist/transitions/placements.js +89 -0
  438. package/dist/transitions/preflightData.d.ts +21 -0
  439. package/dist/transitions/preflightData.js +317 -0
  440. package/dist/transitions/releaseHoldout.d.ts +8 -0
  441. package/dist/transitions/releaseHoldout.js +30 -0
  442. package/dist/transitions/resolveCaps.d.ts +28 -0
  443. package/dist/transitions/resolveCaps.js +68 -0
  444. package/dist/transitions/start.d.ts +26 -0
  445. package/dist/transitions/start.js +220 -0
  446. package/dist/transitions/stop.d.ts +8 -0
  447. package/dist/transitions/stop.js +25 -0
  448. package/dist/types/admin.d.ts +20 -0
  449. package/dist/types/admin.js +1 -0
  450. package/dist/types/assignments.d.ts +57 -0
  451. package/dist/types/assignments.js +1 -0
  452. package/dist/types/contracts.d.ts +141 -0
  453. package/dist/types/contracts.js +1 -0
  454. package/dist/types/endpoints.d.ts +135 -0
  455. package/dist/types/endpoints.js +1 -0
  456. package/dist/types/index.d.ts +8 -0
  457. package/dist/types/index.js +8 -0
  458. package/dist/types/manifest.d.ts +42 -0
  459. package/dist/types/manifest.js +1 -0
  460. package/dist/types/puck.d.ts +23 -0
  461. package/dist/types/puck.js +1 -0
  462. package/dist/types/runtime.d.ts +59 -0
  463. package/dist/types/runtime.js +1 -0
  464. package/dist/types/snapshot.d.ts +117 -0
  465. package/dist/types/snapshot.js +1 -0
  466. package/package.json +138 -3
@@ -0,0 +1,250 @@
1
+ import pg from 'pg';
2
+ /** Scan only SQL code, leaving quoted text, identifiers and nested comments alone.
3
+ * Both transports validate unused/missing params here before touching a database.
4
+ */ export function scanParameters(text, params) {
5
+ const positions = [];
6
+ const used = new Set();
7
+ const identifier = (char)=>char !== undefined && /[\p{L}\p{N}_$]/u.test(char);
8
+ let i = 0;
9
+ while(i < text.length){
10
+ if (text.startsWith('--', i)) {
11
+ const end = text.indexOf('\n', i + 2);
12
+ i = end < 0 ? text.length : end + 1;
13
+ } else if (text.startsWith('/*', i)) {
14
+ let depth = 1;
15
+ i += 2;
16
+ while(i < text.length && depth){
17
+ if (text.startsWith('/*', i)) {
18
+ depth++;
19
+ i += 2;
20
+ } else if (text.startsWith('*/', i)) {
21
+ depth--;
22
+ i += 2;
23
+ } else i++;
24
+ }
25
+ } else if (text[i] === "'" || text[i] === '"') {
26
+ const quote = text[i++];
27
+ const escaped = quote === "'" && /[eE]/.test(text[i - 2] ?? '') && !identifier(text[i - 3]);
28
+ while(i < text.length){
29
+ if (escaped && text[i] === '\\') i += 2;
30
+ else if (text[i++] === quote) {
31
+ if (text[i] === quote) i++;
32
+ else break;
33
+ }
34
+ }
35
+ } else if (text[i] === '$' && !identifier(text[i - 1])) {
36
+ const tag = /^(?:\$[A-Za-z_][A-Za-z_0-9]*\$|\$\$)/.exec(text.slice(i))?.[0];
37
+ if (tag) {
38
+ const end = text.indexOf(tag, i + tag.length);
39
+ i = end < 0 ? text.length : end + tag.length;
40
+ continue;
41
+ }
42
+ const token = /^\$(\d+)/.exec(text.slice(i));
43
+ if (token) {
44
+ const index = Number(token[1]) - 1;
45
+ if (!Number.isSafeInteger(index) || index < 0 || index >= params.length) throw new RangeError(`Missing SQL parameter ${token[0]}`);
46
+ positions.push({
47
+ start: i,
48
+ end: i + token[0].length,
49
+ index
50
+ });
51
+ used.add(index);
52
+ i += token[0].length;
53
+ } else i++;
54
+ } else i++;
55
+ }
56
+ if (used.size !== params.length) throw new RangeError('Unused SQL parameters');
57
+ return positions;
58
+ }
59
+ function numeric(value, integer) {
60
+ if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'bigint' || typeof value === 'string' && !/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i.test(value)) {
61
+ throw new RangeError('Invalid PostgreSQL numeric value');
62
+ }
63
+ const number = Number(value);
64
+ if (!Number.isFinite(number)) throw new RangeError('Non-finite PostgreSQL numeric value');
65
+ if (integer && !Number.isSafeInteger(number)) throw new RangeError('PostgreSQL int8 exceeds the safe integer range');
66
+ return number;
67
+ }
68
+ /** Normalise top-level columns only. Finite numeric (1700) always becomes a
69
+ * number, accepting double precision rounding as the stats engine does. int8 (20)
70
+ * becomes a number only within the safe integer range; invalid/non-finite numeric
71
+ * and unsafe int8 values throw instead of changing a column's JS type by value.
72
+ * Nulls stay null. timestamp/timestamptz (1114/1184) become UTC ISO strings; infinity
73
+ * stays a string. JSON, text and opaque IDs stay intact. Drivers must retain raw
74
+ * numeric text (pg's default, and per-query overrides below) until this boundary.
75
+ */ export function normalise(value) {
76
+ const result = value;
77
+ const types = new Map(result.fields?.map((field)=>[
78
+ field.name,
79
+ field.dataTypeID
80
+ ]));
81
+ const rows = result.rows.map((row)=>Object.fromEntries(Object.entries(row).map(([key, value])=>{
82
+ const type = types.get(key);
83
+ if ((type === 1700 || type === 20) && value !== null) value = numeric(value, type === 20);
84
+ else if (value instanceof Date) value = value.toISOString();
85
+ else if (typeof value === 'string' && (type === 1114 || type === 1184) && !/^-?infinity$/i.test(value)) {
86
+ const timestamp = type === 1114 && !/(?:Z|[+-]\d{2}(?::?\d{2})?)$/.test(value) ? `${value}Z` : value;
87
+ value = new Date(timestamp).toISOString();
88
+ }
89
+ return [
90
+ key,
91
+ value
92
+ ];
93
+ })));
94
+ return {
95
+ rows,
96
+ rowCount: result.rowCount ?? result.affectedRows ?? rows.length
97
+ };
98
+ }
99
+ /** Per-query overrides avoid process-global pg parser changes and local-time
100
+ * timestamp parsing. Other OIDs retain pg's default parsers (including JSON).
101
+ */ const queryTypes = {
102
+ getTypeParser (oid, format) {
103
+ if (format !== 'binary' && [
104
+ 20,
105
+ 1700,
106
+ 1114,
107
+ 1184
108
+ ].includes(oid)) return (value)=>value;
109
+ return pg.types.getTypeParser(oid, format);
110
+ }
111
+ };
112
+ export function pgPoolExecutor(pool) {
113
+ return {
114
+ async query (text, params = []) {
115
+ scanParameters(text, params);
116
+ // QueryConfig is required to scope type parsers to this query.
117
+ return normalise(await pool.query({
118
+ text,
119
+ values: params,
120
+ types: queryTypes
121
+ }));
122
+ }
123
+ };
124
+ }
125
+ /** The caller must release in finally; this connection never joins a Payload transaction. */ export async function ownConnection(pool) {
126
+ const client = await pool.connect();
127
+ const exec = pgPoolExecutor(client);
128
+ let queryError;
129
+ return {
130
+ async query (text, params) {
131
+ try {
132
+ return await exec.query(text, params);
133
+ } catch (error) {
134
+ queryError ??= connectionError(error);
135
+ throw error;
136
+ }
137
+ },
138
+ release: (error)=>client.release(error ?? queryError)
139
+ };
140
+ }
141
+ export const RESULTS_TIMEOUT_MARGIN_MS = 10;
142
+ export class ResultsBudgetError extends Error {
143
+ name = 'ResultsBudgetError';
144
+ constructor(){
145
+ super('Results budget exhausted');
146
+ }
147
+ }
148
+ /** SQL errors leave a usable connection after rollback. Transport failures and
149
+ * deadline races can leave queries in flight, so those clients must be discarded. */ function connectionError(error) {
150
+ const code = error && typeof error === 'object' && 'code' in error ? error.code : undefined;
151
+ if (!(error instanceof ResultsBudgetError) && typeof code === 'string' && /^[0-9A-Z]{5}$/.test(code) && !code.startsWith('08') && !code.startsWith('57P')) return undefined;
152
+ return error instanceof Error ? error : new Error('Query failed', {
153
+ cause: error
154
+ });
155
+ }
156
+ /** Invoke lazily so expired work never starts. Promise.race observes late source
157
+ * rejections too; clearing the timer keeps successful cron calls from lingering. */ export async function raceDeadline(deadline, work) {
158
+ const remaining = deadline - Date.now();
159
+ if (remaining <= 0) throw new ResultsBudgetError();
160
+ let timer;
161
+ try {
162
+ return await Promise.race([
163
+ Promise.resolve().then(work),
164
+ new Promise((_, reject)=>{
165
+ timer = setTimeout(()=>reject(new ResultsBudgetError()), remaining);
166
+ })
167
+ ]);
168
+ } finally{
169
+ clearTimeout(timer);
170
+ }
171
+ }
172
+ export async function withStatementTimeout(pool, ms, fn, deadline) {
173
+ if (!Number.isSafeInteger(ms) || ms <= 0) throw new RangeError('statement timeout must be a positive integer');
174
+ if (deadline !== undefined) {
175
+ if (deadline - Date.now() <= RESULTS_TIMEOUT_MARGIN_MS) throw new ResultsBudgetError();
176
+ let abandoned = false;
177
+ const connection = Promise.resolve().then(()=>pool.connect()).then((client)=>{
178
+ if (abandoned) {
179
+ client.release(new ResultsBudgetError());
180
+ throw new ResultsBudgetError();
181
+ }
182
+ return client;
183
+ });
184
+ let client;
185
+ try {
186
+ client = await raceDeadline(deadline, ()=>connection);
187
+ } catch (error) {
188
+ abandoned = true;
189
+ throw error;
190
+ }
191
+ const exec = pgPoolExecutor(client);
192
+ let releaseError;
193
+ try {
194
+ return await raceDeadline(deadline, async ()=>{
195
+ if (deadline - Date.now() <= RESULTS_TIMEOUT_MARGIN_MS) throw new ResultsBudgetError();
196
+ await exec.query('BEGIN');
197
+ const timeout = Math.min(ms, Math.floor(deadline - Date.now() - RESULTS_TIMEOUT_MARGIN_MS));
198
+ if (timeout <= 0) throw new ResultsBudgetError();
199
+ await exec.query(`SET LOCAL statement_timeout = ${timeout}`);
200
+ if (deadline - Date.now() <= RESULTS_TIMEOUT_MARGIN_MS) throw new ResultsBudgetError();
201
+ const result = await fn(exec);
202
+ if (Date.now() >= deadline) throw new ResultsBudgetError();
203
+ // COMMIT may land while the deadline race reports this experiment as
204
+ // skipped. Harmless: the next results run rewrites the snapshot.
205
+ await exec.query('COMMIT');
206
+ return result;
207
+ });
208
+ } catch (error) {
209
+ const failure = error && typeof error === 'object' && 'code' in error && error.code === '57014' ? new ResultsBudgetError() : error;
210
+ releaseError = connectionError(error);
211
+ // Destroy deadline-raced clients immediately, including BEGIN/SET/COMMIT hangs.
212
+ // A rollback cannot be allowed to extend the caller's deadline either.
213
+ // A server statement timeout (57014) has settled; rollback can reuse it.
214
+ if (!(error instanceof ResultsBudgetError)) {
215
+ try {
216
+ await raceDeadline(deadline, ()=>exec.query('ROLLBACK'));
217
+ } catch (rollbackError) {
218
+ // Even an ordinary SQL error here leaves transaction state uncertain.
219
+ releaseError = rollbackError instanceof Error ? rollbackError : new Error('Rollback failed', {
220
+ cause: rollbackError
221
+ });
222
+ }
223
+ }
224
+ throw failure;
225
+ } finally{
226
+ client.release(releaseError);
227
+ }
228
+ }
229
+ const exec = await ownConnection(pool);
230
+ let releaseError;
231
+ try {
232
+ await exec.query('BEGIN');
233
+ await exec.query(`SET LOCAL statement_timeout = ${ms}`);
234
+ const result = await fn(exec);
235
+ await exec.query('COMMIT');
236
+ return result;
237
+ } catch (error) {
238
+ releaseError = connectionError(error);
239
+ try {
240
+ await exec.query('ROLLBACK');
241
+ } catch (rollbackError) {
242
+ releaseError = rollbackError instanceof Error ? rollbackError : new Error('Rollback failed', {
243
+ cause: rollbackError
244
+ });
245
+ }
246
+ throw error;
247
+ } finally{
248
+ exec.release(releaseError);
249
+ }
250
+ }
@@ -0,0 +1,53 @@
1
+ import type { DocId } from '../types/contracts.js';
2
+ import { type Timestamp } from '../results/aggregate.js';
3
+ import type { Executor } from './executor.js';
4
+ export interface PhaseExposureRow {
5
+ unitId: string;
6
+ arm: string;
7
+ ts: number;
8
+ }
9
+ export interface RolloutExposureRow {
10
+ unitId: string;
11
+ phase: number;
12
+ ts: number;
13
+ }
14
+ export interface MetricEventRow {
15
+ unitId: string;
16
+ userId: string | null;
17
+ value: number;
18
+ ts: number;
19
+ }
20
+ export interface IdentityRow {
21
+ anonymousId: string;
22
+ userId: string;
23
+ }
24
+ type PhaseQuery = {
25
+ experimentId: DocId;
26
+ phase: number;
27
+ endedAt?: Timestamp;
28
+ };
29
+ /** All phase exposures, including multi-arm rows for firstExposures to exclude. */
30
+ export declare function phaseExposureRows(exec: Executor, opts: PhaseQuery): Promise<PhaseExposureRow[]>;
31
+ /** Diagnostic distinct units only; this never contributes to multi-arm exclusion. */
32
+ export declare function armMismatchCount(exec: Executor, opts: PhaseQuery): Promise<number>;
33
+ /** All phases are required to identify first-ever exposure for rollout X1. */
34
+ export declare function rolloutExposureRows(exec: Executor, { experimentId, respectPhaseEnds }: {
35
+ experimentId: DocId;
36
+ respectPhaseEnds?: boolean;
37
+ }): Promise<RolloutExposureRow[]>;
38
+ /** events:<name> conversion rows in [from, to). Choose unitIds for stored unit_id,
39
+ * or userIds for stored user_id (user units and visitor identity credit). Each distinct
40
+ * ID occurs in one ANY array of at most 10,000, so batching cannot duplicate rows.
41
+ */
42
+ export declare function metricEventRows(exec: Executor, { eventName, unitIds, userIds, from, to }: {
43
+ eventName: string;
44
+ unitIds?: readonly string[];
45
+ userIds?: readonly string[];
46
+ from: Timestamp;
47
+ to: Timestamp;
48
+ }): Promise<MetricEventRow[]>;
49
+ /** Linked visitors for creditUserEvents; disjoint ANY batches of ≤ 10,000 IDs. */
50
+ export declare function identityRows(exec: Executor, { anonymousIds }: {
51
+ anonymousIds: readonly string[];
52
+ }): Promise<IdentityRow[]>;
53
+ export {};
@@ -0,0 +1,103 @@
1
+ import { timestampMs } from '../results/aggregate.js';
2
+ import { COLUMNS, TABLES } from './names.js';
3
+ import { qualifiedTable, quoteIdentifier as q } from './sql.js';
4
+ const c = COLUMNS.events;
5
+ const batchSize = 10_000;
6
+ const iso = (ts)=>new Date(timestampMs(ts)).toISOString();
7
+ // round()::bigint keeps exact milliseconds on servers where extract() returns a float (PG ≤ 13).
8
+ const epochMs = `round(extract(epoch from ${q(c.ts)}) * 1000)::bigint`;
9
+ function phaseFilter({ experimentId, phase, endedAt }, type) {
10
+ const params = [
11
+ experimentId,
12
+ phase,
13
+ type
14
+ ];
15
+ let text = `${q(c.experimentId)} = $1 AND ${q(c.phase)} = $2 AND ${q(c.type)} = $3`;
16
+ if (endedAt !== undefined) {
17
+ params.push(iso(endedAt));
18
+ text += ` AND ${q(c.ts)} < $4`;
19
+ }
20
+ return {
21
+ text,
22
+ params
23
+ };
24
+ }
25
+ /** All phase exposures, including multi-arm rows for firstExposures to exclude. */ export async function phaseExposureRows(exec, opts) {
26
+ const filter = phaseFilter(opts, 'exposure');
27
+ const { rows } = await exec.query(`SELECT ${q(c.unitId)} AS "unitId", ${q(c.arm)} AS "arm", ${epochMs} AS "ts" FROM ${qualifiedTable(exec, TABLES.events)} WHERE ${filter.text} ORDER BY ${q(c.ts)}, ${q(c.unitId)}`, filter.params);
28
+ return rows.map((row)=>({
29
+ unitId: row.unitId,
30
+ arm: row.arm,
31
+ ts: Number(row.ts)
32
+ }));
33
+ }
34
+ /** Diagnostic distinct units only; this never contributes to multi-arm exclusion. */ export async function armMismatchCount(exec, opts) {
35
+ const filter = phaseFilter(opts, 'arm-mismatch');
36
+ const { rows } = await exec.query(`SELECT count(DISTINCT ${q(c.unitId)}) AS "unitCount" FROM ${qualifiedTable(exec, TABLES.events)} WHERE ${filter.text}`, filter.params);
37
+ return Number(rows[0].unitCount);
38
+ }
39
+ /** All phases are required to identify first-ever exposure for rollout X1. */ export async function rolloutExposureRows(exec, { experimentId, respectPhaseEnds = false }) {
40
+ const p = COLUMNS.phases;
41
+ const cutoff = respectPhaseEnds ? ` AND NOT EXISTS (SELECT 1 FROM ${qualifiedTable(exec, TABLES.phases)} p WHERE p.${q(p.experimentId)} = ${qualifiedTable(exec, TABLES.events)}.${q(c.experimentId)} AND p.${q(p.phase)} = ${qualifiedTable(exec, TABLES.events)}.${q(c.phase)} AND p.${q(p.endedAt)} <= ${qualifiedTable(exec, TABLES.events)}.${q(c.ts)})` : '';
42
+ const { rows } = await exec.query(`SELECT ${q(c.unitId)} AS "unitId", ${q(c.phase)} AS "phase", ${epochMs} AS "ts" FROM ${qualifiedTable(exec, TABLES.events)} WHERE ${q(c.experimentId)} = $1 AND ${q(c.type)} = $2${cutoff} ORDER BY ${q(c.ts)}, ${q(c.unitId)}, ${q(c.phase)}`, [
43
+ experimentId,
44
+ 'exposure'
45
+ ]);
46
+ return rows.map((row)=>({
47
+ unitId: row.unitId,
48
+ phase: Number(row.phase),
49
+ ts: Number(row.ts)
50
+ }));
51
+ }
52
+ /** events:<name> conversion rows in [from, to). Choose unitIds for stored unit_id,
53
+ * or userIds for stored user_id (user units and visitor identity credit). Each distinct
54
+ * ID occurs in one ANY array of at most 10,000, so batching cannot duplicate rows.
55
+ */ export async function metricEventRows(exec, { eventName, unitIds, userIds, from, to }) {
56
+ if (unitIds !== undefined && userIds !== undefined) throw new RangeError('Choose unitIds or userIds');
57
+ const params = [
58
+ 'conversion',
59
+ eventName,
60
+ iso(from),
61
+ iso(to)
62
+ ];
63
+ const text = `SELECT ${q(c.unitId)} AS "unitId", ${q(c.userId)} AS "userId", ${q(c.value)} AS "value", ${epochMs} AS "ts" FROM ${qualifiedTable(exec, TABLES.events)} WHERE ${q(c.type)} = $1 AND ${q(c.eventName)} = $2 AND ${q(c.ts)} >= $3 AND ${q(c.ts)} < $4`;
64
+ const filterIds = userIds ?? unitIds;
65
+ const column = userIds === undefined ? c.unitId : c.userId;
66
+ const ids = filterIds === undefined ? undefined : [
67
+ ...new Set(filterIds)
68
+ ];
69
+ const rows = [];
70
+ for(let offset = 0; ids === undefined || offset < ids.length; offset += batchSize){
71
+ const result = await exec.query(`${text}${ids === undefined ? '' : ` AND ${q(column)} = ANY($5::text[])`} ORDER BY ${q(c.ts)}, ${q(c.unitId)}`, ids === undefined ? params : [
72
+ ...params,
73
+ ids.slice(offset, offset + batchSize)
74
+ ]);
75
+ for (const row of result.rows){
76
+ rows.push({
77
+ unitId: row.unitId,
78
+ userId: row.userId == null ? null : String(row.userId),
79
+ value: Number(row.value),
80
+ ts: Number(row.ts)
81
+ });
82
+ }
83
+ if (ids === undefined) break;
84
+ }
85
+ return rows;
86
+ }
87
+ /** Linked visitors for creditUserEvents; disjoint ANY batches of ≤ 10,000 IDs. */ export async function identityRows(exec, { anonymousIds }) {
88
+ const ids = [
89
+ ...new Set(anonymousIds)
90
+ ];
91
+ const i = COLUMNS.identities;
92
+ const rows = [];
93
+ for(let offset = 0; offset < ids.length; offset += batchSize){
94
+ const result = await exec.query(`SELECT ${q(i.anonymousId)} AS "anonymousId", ${q(i.userId)} AS "userId" FROM ${qualifiedTable(exec, TABLES.identities)} WHERE ${q(i.anonymousId)} = ANY($1::text[]) ORDER BY ${q(i.anonymousId)}`, [
95
+ ids.slice(offset, offset + batchSize)
96
+ ]);
97
+ rows.push(...result.rows.map((row)=>({
98
+ anonymousId: row.anonymousId,
99
+ userId: String(row.userId)
100
+ })));
101
+ }
102
+ return rows;
103
+ }
@@ -0,0 +1,18 @@
1
+ import type { Executor } from './executor.js';
2
+ import type { DocId } from '../types/contracts.js';
3
+ /** Reclaim exactly the stopped test's slices while holding the layer lock. */
4
+ export declare function reclaimSlices(exec: Executor, { layerId, experimentId, slices }: {
5
+ layerId: DocId;
6
+ experimentId: DocId;
7
+ slices: number[];
8
+ }): Promise<number[]>;
9
+ /** Caller holds the layer lock on this transaction executor. Returns additional
10
+ * free slices; the caller persists them in layerSlices. Existing slices, even
11
+ * those of experimentId itself, stay occupied until explicitly freed with [].
12
+ */
13
+ export declare function claimSlices(exec: Executor, { layerId, experimentId, count, random }: {
14
+ layerId: DocId;
15
+ experimentId: DocId;
16
+ count: number;
17
+ random?: () => number;
18
+ }): Promise<number[]>;
@@ -0,0 +1,51 @@
1
+ import { COLUMNS, TABLES } from './names.js';
2
+ import { XpRefusal } from '../errors.js';
3
+ import { qualifiedTable, quoteIdentifier as quote } from './sql.js';
4
+ /** Reclaim exactly the stopped test's slices while holding the layer lock. */ export async function reclaimSlices(exec, { layerId, experimentId, slices }) {
5
+ const c = COLUMNS.experiments;
6
+ const { rows } = await exec.query(`SELECT ${quote(c.layerSlices)} AS slices FROM ${qualifiedTable(exec, TABLES.experiments)} WHERE ${quote(c.layerId)} = $1 AND ${quote(c.status)} IN ('running', 'decided')`, [
7
+ layerId
8
+ ]);
9
+ const occupied = new Set(rows.flatMap((row)=>row.slices ?? []));
10
+ if (slices.some((slice)=>occupied.has(slice))) throw new XpRefusal('slices-taken', 'The original layer slices are now in use.', {
11
+ layerId,
12
+ experimentId
13
+ }, 422);
14
+ return [
15
+ ...slices
16
+ ];
17
+ }
18
+ /** Caller holds the layer lock on this transaction executor. Returns additional
19
+ * free slices; the caller persists them in layerSlices. Existing slices, even
20
+ * those of experimentId itself, stay occupied until explicitly freed with [].
21
+ */ export async function claimSlices(exec, { layerId, experimentId, count, random = Math.random }) {
22
+ if (!Number.isSafeInteger(count) || count < 0) throw new RangeError('Slice count must be a non-negative integer');
23
+ const table = qualifiedTable(exec, TABLES.experiments);
24
+ const c = COLUMNS.experiments;
25
+ const { rows } = await exec.query(`SELECT ${quote(c.layerSlices)} AS slices FROM ${table} WHERE ${quote(c.layerId)} = $1 AND ${quote(c.status)} IN ('running', 'decided')`, [
26
+ layerId
27
+ ]);
28
+ const occupied = new Set(rows.flatMap((row)=>row.slices ?? []));
29
+ const free = Array.from({
30
+ length: 100
31
+ }, (_, slice)=>slice).filter((slice)=>!occupied.has(slice));
32
+ if (free.length < count) {
33
+ throw new XpRefusal('layer-full', 'There is not enough room in this layer.', {
34
+ layerId,
35
+ experimentId,
36
+ requested: count,
37
+ available: free.length
38
+ }, 409);
39
+ }
40
+ // Partial Fisher–Yates: each subset of the requested size is equally likely.
41
+ for(let i = 0; i < count; i++){
42
+ const value = random();
43
+ if (!(value >= 0 && value < 1)) throw new RangeError('Random must return a number in [0, 1)');
44
+ const j = i + Math.floor(value * (free.length - i));
45
+ [free[i], free[j]] = [
46
+ free[j],
47
+ free[i]
48
+ ];
49
+ }
50
+ return free.slice(0, count).sort((a, b)=>a - b);
51
+ }
@@ -0,0 +1,47 @@
1
+ import type { Payload } from 'payload';
2
+ import type { DocId, UnitType } from '../types/contracts.js';
3
+ import type { Snapshot } from '../types/snapshot.js';
4
+ import type { AnalysisPhase, FrozenMetric } from '../results/analyse.js';
5
+ import { type Executor } from './executor.js';
6
+ export interface ResultsPhase extends AnalysisPhase {
7
+ seed: string;
8
+ allocation: number;
9
+ metricDefinitions: Array<FrozenMetric & {
10
+ source: string;
11
+ keyedBy?: UnitType;
12
+ capNotice?: 'no-history';
13
+ }>;
14
+ }
15
+ export interface ResultsCandidate {
16
+ phase: ResultsPhase;
17
+ unitType: UnitType;
18
+ status: 'running' | 'stopped' | 'decided' | 'archived';
19
+ latestComputedAt: string | null;
20
+ testArms: Array<{
21
+ weight: number;
22
+ }>;
23
+ layerSlug: string | null;
24
+ layerSlices: number[] | null;
25
+ }
26
+ /** Each query gets its own pooled connection and timeout transaction. A failed
27
+ * metric query cannot poison the next metric's transaction. */
28
+ export declare function resultsExecutor(payload: Payload, timeoutMs: () => number): Executor;
29
+ /** Scheduling and immutable assignment metadata only; metric/config data comes
30
+ * from the phase JSON, never from live metric or experiment analysis fields. */
31
+ export declare function resultsCandidates(exec: Executor, { cutoff, experimentId }: {
32
+ cutoff: string;
33
+ experimentId?: DocId;
34
+ }): Promise<ResultsCandidate[]>;
35
+ export declare function writeSnapshot(exec: Executor, snapshot: Snapshot): Promise<void>;
36
+ /** One atomic insert after all phases complete: a budget failure cannot leave
37
+ * the experiment with a partially persisted set of phase snapshots. */
38
+ export declare function writeSnapshots(exec: Executor, snapshots: Snapshot[]): Promise<void>;
39
+ /** Fetch pointers only, then the endpoint reads the bounded set through Payload
40
+ * with req/overrideAccess:false. Called behind the readResults/manage gate. */
41
+ export declare function newestSnapshotIds(exec: Executor, ids: DocId[], per: 'phase' | 'experiment'): Promise<DocId[]>;
42
+ /** Called only behind the endpoint's readResults gate. Experiment collection
43
+ * reads require manage, so read only the decision pointer needed by results. */
44
+ export declare function resultsMetadata(exec: Executor, id: DocId): Promise<{
45
+ status: string;
46
+ snapshotId: DocId | null;
47
+ } | null>;
@@ -0,0 +1,89 @@
1
+ import { withStatementTimeout, ResultsBudgetError, RESULTS_TIMEOUT_MARGIN_MS } from './executor.js';
2
+ import { COLUMNS, TABLES } from './names.js';
3
+ import { qualifiedTable, quoteIdentifier as q } from './sql.js';
4
+ /** Each query gets its own pooled connection and timeout transaction. A failed
5
+ * metric query cannot poison the next metric's transaction. */ export function resultsExecutor(payload, timeoutMs) {
6
+ const db = payload.db;
7
+ return Object.assign({
8
+ query (text, params) {
9
+ const remaining = Math.floor(timeoutMs());
10
+ if (remaining <= RESULTS_TIMEOUT_MARGIN_MS) return Promise.reject(new ResultsBudgetError());
11
+ return withStatementTimeout(db.pool, remaining - RESULTS_TIMEOUT_MARGIN_MS, (exec)=>exec.query(text, params), Date.now() + remaining);
12
+ }
13
+ }, {
14
+ schemaName: db.schemaName
15
+ });
16
+ }
17
+ // CORE-16 README: allow late beacons for 24 hours after a phase ends, then keep
18
+ // its final snapshot frozen. This is internal scheduling policy, not public API.
19
+ const LATE_BEACON_GRACE_MS = 24 * 60 * 60 * 1000;
20
+ /** Scheduling and immutable assignment metadata only; metric/config data comes
21
+ * from the phase JSON, never from live metric or experiment analysis fields. */ export async function resultsCandidates(exec, { cutoff, experimentId }) {
22
+ const p = COLUMNS.phases, e = COLUMNS.experiments, s = COLUMNS.resultsSnapshots, l = COLUMNS.layers;
23
+ const phases = qualifiedTable(exec, TABLES.phases), snapshots = qualifiedTable(exec, TABLES.resultsSnapshots);
24
+ const params = [
25
+ cutoff,
26
+ LATE_BEACON_GRACE_MS
27
+ ];
28
+ if (experimentId !== undefined) params.push(experimentId);
29
+ const { rows } = await exec.query(`SELECT
30
+ jsonb_build_object('experiment', p.${q(p.experimentId)}, 'phase', p.${q(p.phase)}, 'phaseKind', p.${q(p.phaseKind)},
31
+ 'startedAt', p.${q(p.startedAt)}, 'endedAt', p.${q(p.endedAt)}, 'seed', p.${q(p.seed)},
32
+ 'arms', p.${q(p.arms)}, 'allocation', p.${q(p.allocation)}, 'metricDefinitions', p.${q(p.metricDefinitions)},
33
+ 'analysis', p.${q(p.analysis)}, 'contentChanges', p.${q(p.contentChanges)}) AS "phase",
34
+ e.${q(e.unitType)} AS "unitType", e.${q(e.status)} AS "status",
35
+ (SELECT max(s.${q(s.computedAt)}) FROM ${snapshots} s WHERE s.${q(s.experimentId)} = p.${q(p.experimentId)} AND s.${q(s.phase)} = p.${q(p.phase)}) AS "latestComputedAt",
36
+ (SELECT t.${q(p.arms)} FROM ${phases} t WHERE t.${q(p.experimentId)} = p.${q(p.experimentId)} AND t.${q(p.phaseKind)} = 'test' AND t.${q(p.phase)} <= p.${q(p.phase)} ORDER BY t.${q(p.phase)} DESC LIMIT 1) AS "testArms",
37
+ l.${q(l.slug)} AS "layerSlug", e.${q(e.layerSlices)} AS "layerSlices"
38
+ FROM ${phases} p JOIN ${qualifiedTable(exec, TABLES.experiments)} e ON e.${q(e.id)} = p.${q(p.experimentId)}
39
+ LEFT JOIN ${qualifiedTable(exec, TABLES.layers)} l ON l.${q(l.id)} = e.${q(e.layerId)}
40
+ WHERE (p.${q(p.endedAt)} IS NULL OR p.${q(p.endedAt)} >= $1)
41
+ AND NOT EXISTS (SELECT 1 FROM ${snapshots} frozen WHERE frozen.${q(s.experimentId)} = p.${q(p.experimentId)} AND frozen.${q(s.phase)} = p.${q(p.phase)}
42
+ AND frozen.${q(s.computedAt)} > p.${q(p.endedAt)} + $2 * interval '1 millisecond')${experimentId === undefined ? '' : ` AND p.${q(p.experimentId)} = $3`}
43
+ ORDER BY "latestComputedAt" NULLS FIRST, p.${q(p.experimentId)}, p.${q(p.phase)}`, params);
44
+ return rows;
45
+ }
46
+ export async function writeSnapshot(exec, snapshot) {
47
+ return writeSnapshots(exec, [
48
+ snapshot
49
+ ]);
50
+ }
51
+ /** One atomic insert after all phases complete: a budget failure cannot leave
52
+ * the experiment with a partially persisted set of phase snapshots. */ export async function writeSnapshots(exec, snapshots) {
53
+ if (!snapshots.length) return;
54
+ const c = COLUMNS.resultsSnapshots;
55
+ const params = [];
56
+ const values = snapshots.map((snapshot)=>{
57
+ const offset = params.length;
58
+ params.push(snapshot.experiment, snapshot.phase, snapshot.computedAt, JSON.stringify(snapshot.window), JSON.stringify(snapshot));
59
+ return `($${offset + 1}, $${offset + 2}, $${offset + 3}, $${offset + 4}::jsonb, $${offset + 5}::jsonb)`;
60
+ });
61
+ // §7.8: runResults writes are privileged internal side effects, using pooled SQL.
62
+ await exec.query(`INSERT INTO ${qualifiedTable(exec, TABLES.resultsSnapshots)} (${q(c.experimentId)}, ${q(c.phase)}, ${q(c.computedAt)}, ${q(c.window)}, ${q(c.snapshot)}) VALUES ${values.join(', ')}`, params);
63
+ }
64
+ /** Fetch pointers only, then the endpoint reads the bounded set through Payload
65
+ * with req/overrideAccess:false. Called behind the readResults/manage gate. */ export async function newestSnapshotIds(exec, ids, per) {
66
+ if (!ids.length) return [];
67
+ const c = COLUMNS.resultsSnapshots;
68
+ const partition = [
69
+ c.experimentId,
70
+ ...per === 'phase' ? [
71
+ c.phase
72
+ ] : []
73
+ ].map(q).join(', ');
74
+ const params = [];
75
+ const placeholders = ids.map((id)=>{
76
+ params.push(id);
77
+ return `$${params.length}`;
78
+ });
79
+ const { rows } = await exec.query(`SELECT DISTINCT ON (${partition}) ${q(c.id)} AS "id" FROM ${qualifiedTable(exec, TABLES.resultsSnapshots)} WHERE ${q(c.experimentId)} IN (${placeholders.join(', ')}) ORDER BY ${partition}, ${q(c.computedAt)} DESC, ${q(c.id)} DESC`, params);
80
+ return rows.map((row)=>row.id);
81
+ }
82
+ /** Called only behind the endpoint's readResults gate. Experiment collection
83
+ * reads require manage, so read only the decision pointer needed by results. */ export async function resultsMetadata(exec, id) {
84
+ const c = COLUMNS.experiments;
85
+ const { rows } = await exec.query(`SELECT ${q(c.status)} AS "status", ${q(c.decisionSnapshotId)} AS "snapshotId" FROM ${qualifiedTable(exec, TABLES.experiments)} WHERE ${q(c.id)} = $1`, [
86
+ id
87
+ ]);
88
+ return rows[0] ?? null;
89
+ }
@@ -0,0 +1,5 @@
1
+ import type { Executor } from './executor.js';
2
+ /** Quote schema/table/column identifiers; data values still use parameters. */
3
+ export declare const quoteIdentifier: (name: string) => string;
4
+ /** Preserve the schema carried by the request's executor for raw plugin SQL. */
5
+ export declare function qualifiedTable(exec: Executor, name: string): string;
package/dist/db/sql.js ADDED
@@ -0,0 +1,5 @@
1
+ /** Quote schema/table/column identifiers; data values still use parameters. */ export const quoteIdentifier = (name)=>`"${name.replaceAll('"', '""')}"`;
2
+ /** Preserve the schema carried by the request's executor for raw plugin SQL. */ export function qualifiedTable(exec, name) {
3
+ const schemaName = exec.schemaName;
4
+ return `${schemaName ? `${quoteIdentifier(schemaName)}.` : ''}${quoteIdentifier(name)}`;
5
+ }
@@ -0,0 +1,12 @@
1
+ import type { Executor } from './pgExecutor.js';
2
+ import type { Manifest } from '../types/manifest.js';
3
+ /** Always take this lock last: layer → flag point → experiment → state. */
4
+ export declare function bumpRevision(exec: Executor): Promise<number>;
5
+ export declare function writeManifest(exec: Executor, manifest: Manifest): Promise<void>;
6
+ export declare function readState(exec: Executor, options?: {
7
+ schemaName?: string;
8
+ }): Promise<{
9
+ revision: number;
10
+ manifest: Manifest | null;
11
+ flagDefaults: Manifest['flagDefaults'];
12
+ } | null>;