@mistriwork/mistri 1.0.0-rc.12 → 1.0.0-rc.13

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 (221) hide show
  1. package/README.md +4 -3
  2. package/dist/client/assets/LayoutBuilder-MPzkVU2e.js.map +1 -1
  3. package/dist/client/assets/index-D2dHI6hR.js.map +1 -1
  4. package/dist/client/assets/shell-TreN_uLs.js.map +1 -1
  5. package/dist/docs/access-model.md +1 -1
  6. package/dist/docs/build-and-deploy.md +0 -1
  7. package/dist/docs/cli.md +1 -1
  8. package/dist/docs/glossary.md +3 -3
  9. package/dist/docs/query.md +9 -21
  10. package/dist/docs/status.md +3 -3
  11. package/dist/embed/react/chunks/LayoutBuilder-Cbxa_wve.js.map +1 -1
  12. package/dist/embed/react/chunks/app-0UZ1Re1k.js.map +1 -1
  13. package/dist/embed/react/chunks/page-Bx4o1kpQ.js.map +1 -1
  14. package/dist/embed/react/chunks/palette-Bb7Lf6eo.js.map +1 -1
  15. package/dist/embed/react/chunks/shell-DZRACiB9.js.map +1 -1
  16. package/dist/embed/react/chunks/write-BQO4CFc9.js.map +1 -1
  17. package/dist/embed/react/index.js +1 -1
  18. package/dist/server/build/dockerfile.js +1 -1
  19. package/dist/server/cli/usage.js +2 -2
  20. package/dist/ui/index.js +2 -2
  21. package/dist/ui/page.js +2 -2
  22. package/dist/ui/{shared-N4Ej8yRD.js → shared-1PD7TOTJ.js} +5 -5
  23. package/dist/ui/{shared-N4Ej8yRD.js.map → shared-1PD7TOTJ.js.map} +1 -1
  24. package/dist/ui/{shared-D5m86-4o.js → shared-1gMnOu9J.js} +6 -6
  25. package/dist/ui/{shared-D5m86-4o.js.map → shared-1gMnOu9J.js.map} +1 -1
  26. package/dist/ui/{shared-Ctaj8mR2.js → shared-5RQMi4BR.js} +3 -3
  27. package/dist/ui/{shared-Ctaj8mR2.js.map → shared-5RQMi4BR.js.map} +1 -1
  28. package/dist/ui/{shared-DpLeIATZ.js → shared-B-BUF9fx.js} +2 -2
  29. package/dist/ui/{shared-DpLeIATZ.js.map → shared-B-BUF9fx.js.map} +1 -1
  30. package/dist/ui/{shared-DWYnjItP.js → shared-B-MhiPIN.js} +9 -9
  31. package/dist/ui/{shared-DWYnjItP.js.map → shared-B-MhiPIN.js.map} +1 -1
  32. package/dist/ui/{shared-0fNuhSbG.js → shared-B07qIMlE.js} +5 -5
  33. package/dist/ui/{shared-0fNuhSbG.js.map → shared-B07qIMlE.js.map} +1 -1
  34. package/dist/ui/{shared-lYtMadRU.js → shared-B1SAS_J-.js} +3 -3
  35. package/dist/ui/{shared-lYtMadRU.js.map → shared-B1SAS_J-.js.map} +1 -1
  36. package/dist/ui/{shared-CpI0Y8M5.js → shared-B4jgU8h9.js} +6 -6
  37. package/dist/ui/{shared-CpI0Y8M5.js.map → shared-B4jgU8h9.js.map} +1 -1
  38. package/dist/ui/{shared-Cxu1c35l.js → shared-B5aBCUi7.js} +30 -30
  39. package/dist/ui/{shared-Cxu1c35l.js.map → shared-B5aBCUi7.js.map} +1 -1
  40. package/dist/ui/{shared-CAe-US1q.js → shared-B9H7_Pji.js} +5 -5
  41. package/dist/ui/{shared-CAe-US1q.js.map → shared-B9H7_Pji.js.map} +1 -1
  42. package/dist/ui/{shared-Q2bgBaYo.js → shared-BAXr20S-.js} +4 -4
  43. package/dist/ui/{shared-Q2bgBaYo.js.map → shared-BAXr20S-.js.map} +1 -1
  44. package/dist/ui/{shared-CyEefFFb.js → shared-BDDUzH-f.js} +19 -19
  45. package/dist/ui/{shared-CyEefFFb.js.map → shared-BDDUzH-f.js.map} +1 -1
  46. package/dist/ui/{shared-BdhPI6dq.js → shared-BGmkeDeW.js} +3 -3
  47. package/dist/ui/{shared-BdhPI6dq.js.map → shared-BGmkeDeW.js.map} +1 -1
  48. package/dist/ui/{shared-DONIpq2q.js → shared-BHCQ60yN.js} +2 -2
  49. package/dist/ui/{shared-DONIpq2q.js.map → shared-BHCQ60yN.js.map} +1 -1
  50. package/dist/ui/{shared-CNmaAv64.js → shared-BHsjawAG.js} +8 -8
  51. package/dist/ui/{shared-CNmaAv64.js.map → shared-BHsjawAG.js.map} +1 -1
  52. package/dist/ui/{shared-DgjU9kOy.js → shared-BIQuK_nq.js} +4 -4
  53. package/dist/ui/{shared-DgjU9kOy.js.map → shared-BIQuK_nq.js.map} +1 -1
  54. package/dist/ui/{shared-icSQAj21.js → shared-BOe4W2FI.js} +3 -3
  55. package/dist/ui/{shared-icSQAj21.js.map → shared-BOe4W2FI.js.map} +1 -1
  56. package/dist/ui/{shared-BlxoOJSk.js → shared-BPpC08ip.js} +3 -3
  57. package/dist/ui/{shared-BlxoOJSk.js.map → shared-BPpC08ip.js.map} +1 -1
  58. package/dist/ui/{shared-BtiFL37J.js → shared-BdA3LPCB.js} +2 -2
  59. package/dist/ui/{shared-BtiFL37J.js.map → shared-BdA3LPCB.js.map} +1 -1
  60. package/dist/ui/{shared-CvR94vn4.js → shared-Be3nbWsd.js} +11 -11
  61. package/dist/ui/{shared-CvR94vn4.js.map → shared-Be3nbWsd.js.map} +1 -1
  62. package/dist/ui/{shared-Bl_nF_IH.js → shared-Bf30ZLoS.js} +8 -4
  63. package/dist/ui/shared-Bf30ZLoS.js.map +1 -0
  64. package/dist/ui/{shared-DijrJ_V-.js → shared-BguP_BaR.js} +7 -7
  65. package/dist/ui/{shared-DijrJ_V-.js.map → shared-BguP_BaR.js.map} +1 -1
  66. package/dist/ui/{shared-DPAVoWY0.js → shared-BoLbc2WD.js} +17 -2
  67. package/dist/ui/{shared-DPAVoWY0.js.map → shared-BoLbc2WD.js.map} +1 -1
  68. package/dist/ui/{shared-BkdBTYwx.js → shared-BttyOW4W.js} +3 -3
  69. package/dist/ui/{shared-BkdBTYwx.js.map → shared-BttyOW4W.js.map} +1 -1
  70. package/dist/ui/{shared-CQWLz8uT.js → shared-BumxsBlA.js} +10 -10
  71. package/dist/ui/{shared-CQWLz8uT.js.map → shared-BumxsBlA.js.map} +1 -1
  72. package/dist/ui/{shared-Dzu7tFiC.js → shared-Bw73ktBk.js} +3 -3
  73. package/dist/ui/{shared-Dzu7tFiC.js.map → shared-Bw73ktBk.js.map} +1 -1
  74. package/dist/ui/{shared-BhAYErSx.js → shared-C1B0GH0e.js} +4 -4
  75. package/dist/ui/{shared-BhAYErSx.js.map → shared-C1B0GH0e.js.map} +1 -1
  76. package/dist/ui/{shared-BQJQcGgj.js → shared-C2FsyYr7.js} +4 -4
  77. package/dist/ui/{shared-BQJQcGgj.js.map → shared-C2FsyYr7.js.map} +1 -1
  78. package/dist/ui/{shared-B9lw2Ryh.js → shared-C2RU1qjv.js} +4 -4
  79. package/dist/ui/{shared-B9lw2Ryh.js.map → shared-C2RU1qjv.js.map} +1 -1
  80. package/dist/ui/{shared-DQYNI9wO.js → shared-C42f07hJ.js} +5 -5
  81. package/dist/ui/{shared-DQYNI9wO.js.map → shared-C42f07hJ.js.map} +1 -1
  82. package/dist/ui/{shared-CqCZ8wMY.js → shared-C4LkPCU4.js} +3 -3
  83. package/dist/ui/{shared-CqCZ8wMY.js.map → shared-C4LkPCU4.js.map} +1 -1
  84. package/dist/ui/{shared-BhtpgOpa.js → shared-C4wSfz6k.js} +9 -9
  85. package/dist/ui/{shared-BhtpgOpa.js.map → shared-C4wSfz6k.js.map} +1 -1
  86. package/dist/ui/{shared-BHc2Izkj.js → shared-C9DZtSFH.js} +2 -2
  87. package/dist/ui/{shared-BHc2Izkj.js.map → shared-C9DZtSFH.js.map} +1 -1
  88. package/dist/ui/{shared-DW1YuiNS.js → shared-CA52ClGo.js} +2 -2
  89. package/dist/ui/{shared-DW1YuiNS.js.map → shared-CA52ClGo.js.map} +1 -1
  90. package/dist/ui/{shared-BbSbilkh.js → shared-CBciTwm6.js} +9 -9
  91. package/dist/ui/{shared-BbSbilkh.js.map → shared-CBciTwm6.js.map} +1 -1
  92. package/dist/ui/{shared-C-J32eBn.js → shared-CL3RLKDA.js} +16 -16
  93. package/dist/ui/{shared-C-J32eBn.js.map → shared-CL3RLKDA.js.map} +1 -1
  94. package/dist/ui/{shared-Ck8S4OxW.js → shared-CLq3-_hW.js} +3 -3
  95. package/dist/ui/{shared-Ck8S4OxW.js.map → shared-CLq3-_hW.js.map} +1 -1
  96. package/dist/ui/{shared-Q5WefT7G.js → shared-CMjK-Ucd.js} +3 -3
  97. package/dist/ui/{shared-Q5WefT7G.js.map → shared-CMjK-Ucd.js.map} +1 -1
  98. package/dist/ui/{shared-D3MAuEA3.js → shared-CPjpSFGh.js} +2 -2
  99. package/dist/ui/{shared-D3MAuEA3.js.map → shared-CPjpSFGh.js.map} +1 -1
  100. package/dist/ui/{shared-VshXGwXP.js → shared-CSPhENq3.js} +2 -2
  101. package/dist/ui/{shared-VshXGwXP.js.map → shared-CSPhENq3.js.map} +1 -1
  102. package/dist/ui/{shared-DJrJaE73.js → shared-CZJE4Bmp.js} +3 -3
  103. package/dist/ui/{shared-DJrJaE73.js.map → shared-CZJE4Bmp.js.map} +1 -1
  104. package/dist/ui/{shared-D7ONOwI7.js → shared-C_BhUL2I.js} +7 -7
  105. package/dist/ui/{shared-D7ONOwI7.js.map → shared-C_BhUL2I.js.map} +1 -1
  106. package/dist/ui/{shared-j-2peBIB.js → shared-CbeujwdU.js} +4 -4
  107. package/dist/ui/{shared-j-2peBIB.js.map → shared-CbeujwdU.js.map} +1 -1
  108. package/dist/ui/{shared-D5GrOEa2.js → shared-Cbf-o4Dt.js} +4 -4
  109. package/dist/ui/{shared-D5GrOEa2.js.map → shared-Cbf-o4Dt.js.map} +1 -1
  110. package/dist/ui/{shared-HwiPIajr.js → shared-CdNmeNHh.js} +3 -3
  111. package/dist/ui/{shared-HwiPIajr.js.map → shared-CdNmeNHh.js.map} +1 -1
  112. package/dist/ui/{shared-ZCFt-CPb.js → shared-CdQOMD8J.js} +3 -3
  113. package/dist/ui/{shared-ZCFt-CPb.js.map → shared-CdQOMD8J.js.map} +1 -1
  114. package/dist/ui/{shared-ClhXSGGK.js → shared-CdiPYlTh.js} +2 -2
  115. package/dist/ui/{shared-ClhXSGGK.js.map → shared-CdiPYlTh.js.map} +1 -1
  116. package/dist/ui/{shared-DA8_pVRZ.js → shared-CepLAhwm.js} +2 -2
  117. package/dist/ui/{shared-DA8_pVRZ.js.map → shared-CepLAhwm.js.map} +1 -1
  118. package/dist/ui/{shared-BrYKjrnM.js → shared-Ch82h_Fu.js} +6 -6
  119. package/dist/ui/{shared-BrYKjrnM.js.map → shared-Ch82h_Fu.js.map} +1 -1
  120. package/dist/ui/{shared-C4UrE8Fb.js → shared-Chp88all.js} +3 -3
  121. package/dist/ui/{shared-C4UrE8Fb.js.map → shared-Chp88all.js.map} +1 -1
  122. package/dist/ui/{shared-CWa80DTO.js → shared-Ci16s2rN.js} +9 -9
  123. package/dist/ui/{shared-CWa80DTO.js.map → shared-Ci16s2rN.js.map} +1 -1
  124. package/dist/ui/{shared-tksTLD6Q.js → shared-CiqNkoE1.js} +5 -5
  125. package/dist/ui/{shared-tksTLD6Q.js.map → shared-CiqNkoE1.js.map} +1 -1
  126. package/dist/ui/{shared-yt7DjSrq.js → shared-CpMhOq8F.js} +2 -2
  127. package/dist/ui/{shared-yt7DjSrq.js.map → shared-CpMhOq8F.js.map} +1 -1
  128. package/dist/ui/{shared-Dm0z5WUl.js → shared-CrWv5JKH.js} +6 -6
  129. package/dist/ui/{shared-Dm0z5WUl.js.map → shared-CrWv5JKH.js.map} +1 -1
  130. package/dist/ui/{shared-CQL3FGHc.js → shared-Csfi3Pti.js} +2 -2
  131. package/dist/ui/{shared-CQL3FGHc.js.map → shared-Csfi3Pti.js.map} +1 -1
  132. package/dist/ui/{shared-BKNRbm-V.js → shared-CuZt26rm.js} +2 -2
  133. package/dist/ui/{shared-BKNRbm-V.js.map → shared-CuZt26rm.js.map} +1 -1
  134. package/dist/ui/{shared-Guq3upiR.js → shared-Cws_z-TS.js} +2 -2
  135. package/dist/ui/{shared-Guq3upiR.js.map → shared-Cws_z-TS.js.map} +1 -1
  136. package/dist/ui/{shared-Bw_qZeIj.js → shared-D1KWuCg6.js} +2 -2
  137. package/dist/ui/{shared-Bw_qZeIj.js.map → shared-D1KWuCg6.js.map} +1 -1
  138. package/dist/ui/{shared-CuCZKwXW.js → shared-D66ju_Cb.js} +23 -23
  139. package/dist/ui/{shared-CuCZKwXW.js.map → shared-D66ju_Cb.js.map} +1 -1
  140. package/dist/ui/{shared-DYfX7NSM.js → shared-DGaQZXnw.js} +2 -2
  141. package/dist/ui/{shared-DYfX7NSM.js.map → shared-DGaQZXnw.js.map} +1 -1
  142. package/dist/ui/{shared-B3Hp8tOv.js → shared-DHy41sET.js} +6 -6
  143. package/dist/ui/{shared-B3Hp8tOv.js.map → shared-DHy41sET.js.map} +1 -1
  144. package/dist/ui/{shared-CXyd7IKi.js → shared-DKEOMQ6l.js} +2 -2
  145. package/dist/ui/{shared-CXyd7IKi.js.map → shared-DKEOMQ6l.js.map} +1 -1
  146. package/dist/ui/{shared-Ccmc8YO0.js → shared-DSUICuaJ.js} +33 -33
  147. package/dist/ui/{shared-Ccmc8YO0.js.map → shared-DSUICuaJ.js.map} +1 -1
  148. package/dist/ui/{shared-CL2iw_js.js → shared-DUPMRoO0.js} +13 -13
  149. package/dist/ui/{shared-CL2iw_js.js.map → shared-DUPMRoO0.js.map} +1 -1
  150. package/dist/ui/{shared-Dtp4eRul.js → shared-DVfdm6ah.js} +2 -2
  151. package/dist/ui/{shared-Dtp4eRul.js.map → shared-DVfdm6ah.js.map} +1 -1
  152. package/dist/ui/{shared-BJ5Ex32G.js → shared-DXM7d7lJ.js} +3 -3
  153. package/dist/ui/{shared-BJ5Ex32G.js.map → shared-DXM7d7lJ.js.map} +1 -1
  154. package/dist/ui/{shared-BFr6ld0e.js → shared-DXbMlcev.js} +2 -2
  155. package/dist/ui/{shared-BFr6ld0e.js.map → shared-DXbMlcev.js.map} +1 -1
  156. package/dist/ui/{shared-BUp3jv-N.js → shared-D_SkPtJ9.js} +6 -6
  157. package/dist/ui/{shared-BUp3jv-N.js.map → shared-D_SkPtJ9.js.map} +1 -1
  158. package/dist/ui/{shared-CdNomVzH.js → shared-Dd21k2ic.js} +4 -4
  159. package/dist/ui/{shared-CdNomVzH.js.map → shared-Dd21k2ic.js.map} +1 -1
  160. package/dist/ui/{shared-gRMbxmpu.js → shared-Dh0nHtLZ.js} +8 -8
  161. package/dist/ui/{shared-gRMbxmpu.js.map → shared-Dh0nHtLZ.js.map} +1 -1
  162. package/dist/ui/{shared-BKGJgjog.js → shared-DkOdSpSK.js} +3 -3
  163. package/dist/ui/{shared-BKGJgjog.js.map → shared-DkOdSpSK.js.map} +1 -1
  164. package/dist/ui/{shared-BMsh72Uq.js → shared-Dkph5ckT.js} +2 -2
  165. package/dist/ui/{shared-BMsh72Uq.js.map → shared-Dkph5ckT.js.map} +1 -1
  166. package/dist/ui/{shared-Be1HpDOB.js → shared-Dl1_Nx--.js} +3 -3
  167. package/dist/ui/{shared-Be1HpDOB.js.map → shared-Dl1_Nx--.js.map} +1 -1
  168. package/dist/ui/{shared-BQIZWOoJ.js → shared-Dm0IGbAf.js} +2 -2
  169. package/dist/ui/{shared-BQIZWOoJ.js.map → shared-Dm0IGbAf.js.map} +1 -1
  170. package/dist/ui/{shared-DSUtcU5S.js → shared-DnIkTpPr.js} +5 -5
  171. package/dist/ui/{shared-DSUtcU5S.js.map → shared-DnIkTpPr.js.map} +1 -1
  172. package/dist/ui/{shared-DsxGbYlV.js → shared-Docl_0tq.js} +3 -3
  173. package/dist/ui/{shared-DsxGbYlV.js.map → shared-Docl_0tq.js.map} +1 -1
  174. package/dist/ui/{shared-DZskrV26.js → shared-DprrHC72.js} +2 -2
  175. package/dist/ui/{shared-DZskrV26.js.map → shared-DprrHC72.js.map} +1 -1
  176. package/dist/ui/{shared-Bmw43Hwf.js → shared-DyKsJTZs.js} +2 -2
  177. package/dist/ui/{shared-Bmw43Hwf.js.map → shared-DyKsJTZs.js.map} +1 -1
  178. package/dist/ui/{shared-BLG88Pya.js → shared-FO7vfI3F.js} +4 -4
  179. package/dist/ui/{shared-BLG88Pya.js.map → shared-FO7vfI3F.js.map} +1 -1
  180. package/dist/ui/{shared-DLSd80D1.js → shared-HalAVHjv.js} +3 -3
  181. package/dist/ui/{shared-DLSd80D1.js.map → shared-HalAVHjv.js.map} +1 -1
  182. package/dist/ui/{shared-D1mMBLdj.js → shared-JvcI-LWp.js} +15 -15
  183. package/dist/ui/{shared-D1mMBLdj.js.map → shared-JvcI-LWp.js.map} +1 -1
  184. package/dist/ui/{shared-C6Hv_4O0.js → shared-K7Ym27yg.js} +2 -2
  185. package/dist/ui/{shared-C6Hv_4O0.js.map → shared-K7Ym27yg.js.map} +1 -1
  186. package/dist/ui/{shared-B39ln7oi.js → shared-SzlnwfJO.js} +3 -3
  187. package/dist/ui/{shared-B39ln7oi.js.map → shared-SzlnwfJO.js.map} +1 -1
  188. package/dist/ui/{shared-DgPn_Jll.js → shared-TrGbd1FY.js} +3 -3
  189. package/dist/ui/{shared-DgPn_Jll.js.map → shared-TrGbd1FY.js.map} +1 -1
  190. package/dist/ui/{shared-DrFb-XtA.js → shared-WhbuObEW.js} +2 -2
  191. package/dist/ui/{shared-DrFb-XtA.js.map → shared-WhbuObEW.js.map} +1 -1
  192. package/dist/ui/{shared-DiFQljcq.js → shared-XcSKDp6N.js} +11 -11
  193. package/dist/ui/{shared-DiFQljcq.js.map → shared-XcSKDp6N.js.map} +1 -1
  194. package/dist/ui/{shared-CMAg58rY.js → shared-Z4uBJU6I.js} +7 -6
  195. package/dist/ui/shared-Z4uBJU6I.js.map +1 -0
  196. package/dist/ui/{shared-BnUptuUp.js → shared-bAgeiRa5.js} +2 -2
  197. package/dist/ui/{shared-BnUptuUp.js.map → shared-bAgeiRa5.js.map} +1 -1
  198. package/dist/ui/{shared-BxMvSjey.js → shared-cgXG7OgZ.js} +5 -5
  199. package/dist/ui/{shared-BxMvSjey.js.map → shared-cgXG7OgZ.js.map} +1 -1
  200. package/dist/ui/{shared-u4CG2kQJ.js → shared-eZ0SB_zm.js} +2 -2
  201. package/dist/ui/{shared-u4CG2kQJ.js.map → shared-eZ0SB_zm.js.map} +1 -1
  202. package/dist/ui/{shared-L90bWW8e.js → shared-frKFb79C.js} +2 -2
  203. package/dist/ui/{shared-L90bWW8e.js.map → shared-frKFb79C.js.map} +1 -1
  204. package/dist/ui/{shared-Dd34nhkw.js → shared-hAayNSRA.js} +3 -3
  205. package/dist/ui/{shared-Dd34nhkw.js.map → shared-hAayNSRA.js.map} +1 -1
  206. package/dist/ui/{shared-DzC64cpp.js → shared-j_J16p2B.js} +2 -2
  207. package/dist/ui/{shared-DzC64cpp.js.map → shared-j_J16p2B.js.map} +1 -1
  208. package/dist/ui/{shared-DX7HRIV-.js → shared-tqpl5N3g.js} +6 -6
  209. package/dist/ui/{shared-DX7HRIV-.js.map → shared-tqpl5N3g.js.map} +1 -1
  210. package/dist/ui/{shared-BS5dBMpA.js → shared-y-6_ZDts.js} +2 -2
  211. package/dist/ui/{shared-BS5dBMpA.js.map → shared-y-6_ZDts.js.map} +1 -1
  212. package/dist/ui/{shared-Cv5v_MP1.js → shared-zG6e1Rhz.js} +13 -13
  213. package/dist/ui/{shared-Cv5v_MP1.js.map → shared-zG6e1Rhz.js.map} +1 -1
  214. package/dist/ui/testing.js +1 -1
  215. package/package.json +4 -4
  216. package/dist/docs/cli-browser.md +0 -232
  217. package/dist/docs/database-browser.md +0 -115
  218. package/dist/docs/keyboard.md +0 -63
  219. package/dist/docs/mcp.md +0 -86
  220. package/dist/ui/shared-Bl_nF_IH.js.map +0 -1
  221. package/dist/ui/shared-CMAg58rY.js.map +0 -1
@@ -1 +0,0 @@
1
- {"version":3,"file":"shared-Bl_nF_IH.js","names":["say","say","say"],"sources":["../../../kit/src/client/copy/record/savedPages.ts","../../../kit/src/client/savedPages.js","../../../kit/src/client/copy/record/page.ts","../../../kit/src/client/copy/record/components/pageModel.ts","../../../kit/src/client/components/pageModel.ts","../../../kit/src/client/page.js","../../../engine/src/shared/breakdown.ts"],"sourcesContent":["/** What savedPages says, in the record room (packages/kit/src/client/messages.ts says how the catalog is split). */\nimport { defineMessages } from '../../messages.js';\n\nexport const say = defineMessages({\n theServerDidNotConfirm: 'The server did not confirm the change within 30 seconds. It may still complete.',\n noDatabaseConnectionIsSelected: 'No database connection is selected.',\n});\n","/**\n * The pages saved for a connection, as a synchronous mirror of the server's\n * file: what the record page's page list reads (page.js) in either shell, and\n * what the Workbench's page builder (pagebuilder.js) writes. An App reads\n * none — its pages are its definition — and a public visitor has none.\n */\nimport { appRequestScope, ownedBy } from './requestIdentity.js';\nimport { api, loadJson, saveJson, state } from './core.js';\nimport { sessionIdentity, subscribeSessionIdentity } from './sessionIdentity.js';\nimport { shell } from './shellMode.js';\nimport { say } from './copy/record/savedPages.js';\n\nconst KEY = 'mistri.pages.saved';\n\n/**\n * Told when a page is saved or deleted, so the record-page cache and the menu\n * can be rebuilt. A callback rather than an import: page.js reads the saved\n * pages from here, and an import back made a cycle. page.js registers itself.\n * @type {(() => void) | null}\n */\nlet pagesChanged = null;\n/** @param {() => void} fn */\nexport function whenSavedPagesChange(fn) {\n pagesChanged = fn;\n}\n\nconst storeKey = (connection) => `${KEY}.${connection ?? ''}`;\n/** @type {Map<string, Record<string, import('@mistriwork/engine/shared/page.js').PageDef>>} */\nconst mirrors = new Map();\nconst loads = new Map();\n\n/* Another person's pages are not this one's: a change of who is signed in forgets the mirror and any read in flight. */\nsubscribeSessionIdentity(() => {\n mirrors.clear();\n loads.clear();\n});\n\nfunction mirrorFor(connection) {\n if (!mirrors.has(connection)) mirrors.set(connection, loadJson(storeKey(connection), {}));\n return mirrors.get(connection);\n}\n\nfunction updateMirror(connection, pages) {\n mirrors.set(connection, pages);\n // Disk acknowledgement is authoritative; unavailable browser storage is only a cache miss.\n saveJson(storeKey(connection), pages);\n}\n\nasync function writePage(id, page, connection) {\n let timer;\n try {\n await Promise.race([\n api('/api/pages/save', { id, page, ...ownedBy(connection) }),\n new Promise((_resolve, reject) => {\n timer = setTimeout(() => reject(new Error(say('theServerDidNotConfirm'))), 30_000);\n }),\n ]);\n } finally {\n clearTimeout(timer);\n }\n}\n\n/** Pages saved for the connection in hand, read from the local mirror. */\nexport function savedPages(tableId) {\n const all = mirrorFor(state.activeConnection);\n const pages = Object.values(all);\n return tableId ? pages.filter((p) => p.base === tableId) : pages;\n}\n\n/**\n * Pull this connection's saved pages from the server into the mirror.\n *\n * The same arrangement record layouts have, for the same reason: the mirror\n * is what makes reads synchronous, the file is what makes the work outlive\n * this browser. A failure is silent — the mirror already holds whatever this\n * browser saved.\n */\nexport async function loadSavedPages() {\n // A public visitor has none to read (the shell's publicVisitor), and saved pages are\n // the database browser's: an App's pages are its definition.\n if (shell().publicVisitor() || appRequestScope() !== false) return;\n const owner = sessionIdentity();\n const connection = state.activeConnection;\n const before = mirrorFor(connection);\n const load = Symbol();\n loads.set(connection, load);\n try {\n const { pages } = await api('/api/pages/saved');\n // A late read must not undo an acknowledged write or a newer read.\n if (\n owner === sessionIdentity() &&\n pages &&\n typeof pages === 'object' &&\n !Array.isArray(pages) &&\n mirrors.get(connection) === before &&\n loads.get(connection) === load\n )\n updateMirror(connection, pages);\n } catch {\n /* Server-side pages unavailable; the mirror stands. */\n }\n}\n\nexport async function savePage(page, connection = state.activeConnection) {\n const owner = sessionIdentity();\n if (!connection) throw new Error(say('noDatabaseConnectionIsSelected'));\n const saved = structuredClone(page);\n await writePage(saved.id, saved, connection);\n if (owner !== sessionIdentity()) return;\n updateMirror(connection, { ...mirrorFor(connection), [saved.id]: saved });\n /* Also tells the menu to rebuild: it was registered once at boot, so a page\n built afterwards was reachable only from the link you were already on,\n and a deleted one stayed listed as a command that opened nothing. */\n if (state.activeConnection === connection) pagesChanged?.();\n}\n\nexport async function deletePage(id, connection = state.activeConnection) {\n const owner = sessionIdentity();\n if (!connection) throw new Error(say('noDatabaseConnectionIsSelected'));\n await writePage(id, null, connection);\n if (owner !== sessionIdentity()) return;\n const all = { ...mirrorFor(connection) };\n delete all[id];\n updateMirror(connection, all);\n if (state.activeConnection === connection) pagesChanged?.();\n}\n","/** What page says, in the record page room (packages/kit/src/client/messages.ts says how the catalog is split). */\nimport { defineMessages } from '../../messages.js';\n\nexport const say = defineMessages({\n theConnectionOrPage: 'The connection or page definitions changed while loading.',\n thereIsNoPage: 'There is no page called \"{id}\".',\n});\n","/** What pageModel says, in the record page room (packages/kit/src/client/messages.ts says how the catalog is split). */\nimport { defineMessages } from '../../../messages.js';\n\nexport const say = defineMessages({\n savedOnThisServer: 'saved on this server, for this connection',\n fromTheAppOrConnection: 'from the app or connection configuration',\n builtFromTheSchema: 'built from the schema',\n});\n","/**\n * What a page is made of, before anything is drawn: the shapes the server\n * hands back, and the small rules the page and its sections share.\n */\nimport { ownedBy } from '../requestIdentity.js';\nimport { api } from '../core.js';\nimport { whenBody } from '@mistriwork/engine/shared/recordReads.js';\nimport type { TableMeta } from '@mistriwork/engine/shared/schema.js';\nimport type { FigureFormat } from '@mistriwork/engine/shared/model.js';\nimport type { RelatedCreate } from '@mistriwork/engine/schema/model.js';\nimport type { ResolvedWorkflowStatus } from '@mistriwork/engine/shared/page.js';\nimport type { HttpViewDefinition, HttpBreakdownDefinition } from '@mistriwork/engine/shared/resourceHttp.js';\nimport { say } from '../copy/record/components/pageModel.js';\n\nexport interface PageSection {\n sizing?: import('@mistriwork/engine/shared/tableSizing.js').TableSizing;\n kind: import('@mistriwork/engine/shared/pageModel.js').PageSection['kind'];\n /** A chart's shape, height and goal, as a dashboard chart's. */\n shape?: 'line' | 'bar' | 'donut' | 'stacked';\n /** A bar chart's or donut's category order (shared/pageModel.ts, PageChart.sort). */\n sort?: string;\n height?: number;\n target?: number;\n /** A note's words, mark and one way on. */\n text?: string;\n icon?: import('@mistriwork/engine/shared/appIcons.js').AppIconName;\n action?: import('@mistriwork/engine/shared/appLinks.js').AppLinkAction;\n /** An embed: the key of the list view this related list draws. */\n view?: string;\n /** A custom section: the author's component, the record's fields it reads, its named reads and props. */\n component?: import('@mistriwork/engine/shared/appComponents.js').ComponentDecl;\n record?: string[];\n reads?: Record<string, import('@mistriwork/engine/shared/appComponents.js').PageCustomRead>;\n aggregates?: Record<string, import('@mistriwork/engine/shared/appComponents.js').PageCustomTotal>;\n props?: Record<string, unknown>;\n region?: 'main' | 'aside';\n /** The section strip's tab this section is drawn in. */\n tab?: string;\n title?: string;\n /** The tab and title in the source catalog's words, where the reader's differ (server apps.ts keepAddresses): what an address calls its tab. */\n addressWords?: { tab?: string; title?: string };\n from?: string;\n of?: string;\n path?: string;\n by?: string;\n /** A timeline's streams, as the page model keeps them. */\n streams?: Array<{\n from: string;\n path: string;\n at: string;\n show: string;\n detail?: string;\n by?: string;\n filter?: string;\n side?: string;\n /** Every row of the stream in this tone, and drawn as one line: in place of the section's rules. */\n tone?: import('@mistriwork/engine/shared/pageModel.js').TimelineTone;\n compact?: true;\n }>;\n /** A timeline read as a conversation: oldest first, its composer under the rows, rows tinted or drawn as one line by a column's value. */\n newest?: 'last';\n composer?: import('@mistriwork/engine/shared/pageModel.js').TimelineComposer;\n tone?: Record<string, Record<string, import('@mistriwork/engine/shared/pageModel.js').TimelineTone>>;\n compact?: Record<string, string[]>;\n /** The other party's rows, drawn against the far edge: asked by the server as each stream's `side` read (SectionPlan). */\n side?: string;\n filter?: string;\n fn?: string;\n column?: string;\n /** A metric's figure said as a dashboard's: `percent` reads it as 0–100, and a unit word follows it. */\n format?: FigureFormat;\n unit?: string;\n paths?: string[];\n names?: boolean;\n /** A note: quiet small print, or a callout on a ground of its own. */\n variant?: 'quiet' | 'callout';\n /** Properties edited in place, through an action where the reader has no write grant. */\n edit?: true;\n presentation?:\n | 'summary'\n | 'gallery'\n | 'headline'\n | 'facts'\n | 'table'\n | 'properties'\n | 'card'\n | 'header'\n | import('@mistriwork/engine/shared/appCards.js').AppCardsList\n | import('@mistriwork/engine/shared/appComponents.js').CustomListPresentation;\n /** An embedded list view's board or calendar, held to the record (shared/pageModel.ts, PageList.lanes). */\n lanes?: import('@mistriwork/engine/shared/pageModel.js').PageList['lanes'];\n actions?: import('@mistriwork/engine/shared/pageModel.js').PageHeadlineAction[];\n /** A record header's status column, fact words, filled verb and destructive verbs (shared/pageModel.ts). */\n status?: string;\n factLabels?: Record<string, import('@mistriwork/engine/shared/appCards.js').FactLabel>;\n primary?: string;\n destructive?: string[];\n headerActions?: string[];\n /** A headline's second figure, its figure's earlier value, and small print. */\n subfigure?: string;\n was?: string;\n note?: string;\n /** A similar list: rows of the page's own table sharing the record's `same` values, `near` its numbers. */\n similar?: import('@mistriwork/engine/shared/pageModel.js').PageSimilar;\n workflow?: ResolvedWorkflowStatus;\n create?: RelatedCreate<string>;\n empty?: import('@mistriwork/engine/shared/listPresentations.js').AppListEmpty;\n /** Authored `empty: 'hide' | 'collapse'`: the list is left out, or said in one line, while it has no rows. */\n emptyMode?: 'hide' | 'collapse';\n /** The rows under a header for each record they share (shared/pageModel.ts, PageGroup); `limit` is then the rows to a group. */\n group?: import('@mistriwork/engine/shared/pageModel.js').PageGroup;\n /** Columns summed in the list's footer, over every row it matches. */\n totals?: string[];\n /** A ⋯ at the end of each row with the row's own verbs. */\n rowActions?: true;\n /** A related list's authored `create: false`, and the record's verbs it draws in its head (shared/pageModel.ts). */\n noCreate?: true;\n listActions?: string[];\n columns?: string[] | number;\n limit?: number;\n width?: string;\n offsetLeft?: string;\n offsetRight?: string;\n when?: string;\n hidden?: boolean;\n}\n\nexport interface PageDef {\n id: string;\n name: string;\n base: string;\n source: 'saved' | 'config' | 'suggested';\n sections: PageSection[];\n listTabs?: boolean;\n empty?: 'hide' | 'collapse';\n /** Its archetype (shared/pageLayouts.ts): which regions it has and their order. Absent, or 'record', is the record's own. */\n layout?: import('@mistriwork/engine/shared/pageLayouts.js').PageLayoutWord;\n error?: string;\n}\n\nexport type ViewDef = HttpViewDefinition;\n\nexport interface SectionPlan {\n index: number;\n error?: string;\n empty?: boolean;\n view?: ViewDef;\n query?: string;\n viewText?: string;\n counts?: boolean;\n /** A related list's footer sums: one row measured over every related row, a column's or a rollup's. */\n totals?: HttpBreakdownDefinition;\n /** A grouped list's groups: one row for each record its rows share, with their counts and subtotals. */\n groups?: HttpBreakdownDefinition;\n breakdown?: HttpBreakdownDefinition;\n streams?: Array<{\n error?: string;\n empty?: boolean;\n view?: ViewDef;\n from: string;\n at: string;\n show: string;\n detail?: string;\n by?: string;\n /** A timeline's `side`: the stream's key columns, and its read narrowed to the other party's rows. */\n key?: string[];\n side?: { error?: string; empty?: boolean; view?: ViewDef };\n }>;\n /** A custom section's reads, each planned as a list, by name. */\n reads?: Record<string, { error?: string; empty?: boolean; view?: ViewDef }>;\n /** A custom section's aggregate reads, each planned as a breakdown, by name. */\n aggregates?: Record<string, { error?: string; empty?: boolean; breakdown?: HttpBreakdownDefinition }>;\n}\n\nexport interface PageEntry {\n kind: 'page';\n pageId: string;\n table?: string;\n key?: Record<string, unknown>;\n label?: string;\n tab?: string;\n rootBody?: string;\n chooserQuery?: string;\n}\n\n/** A key, when there is no label to show instead. */\nexport function rootLabel(key: Record<string, unknown>): string {\n return Object.values(key).join(' · ');\n}\n\n/**\n * Whether the root record satisfies a condition written in the query\n * language — the `when` of a section or an action. One bounded query,\n * key and condition composed the way every fold is.\n */\nexport async function rootMatches(base: TableMeta, key: Record<string, unknown>, when: string, connection?: string): Promise<boolean> {\n try {\n /* Built as the server builds it for the page's compose (shared/recordReads.ts). */\n const data = await api('/api/run', { ...whenBody(base, key, when), ...(connection ? ownedBy(connection) : {}) });\n return Boolean(data.rows?.length);\n } catch {\n return false;\n }\n}\n\n/**\n * The column values every row of a list already satisfies: a page's list is\n * \"the children of this record\", one equality on the child's reference\n * column that is deliberately not shown, so it has to be put back before\n * anything reads the row's key. One group only, bare `=` on the view's own\n * base.\n */\nexport function pinnedBy(view: ViewDef | undefined): Record<string, unknown> {\n const groups = view?.filter?.groups ?? [];\n if (groups.length !== 1) return {};\n const pinned: Record<string, unknown> = {};\n for (const condition of groups[0]) {\n if (condition.op !== '=' || typeof condition.path !== 'string') continue;\n if (condition.path.includes('.')) continue;\n pinned[condition.path] = condition.value;\n }\n return pinned;\n}\n\n/** The classes a section wears, on the skeleton and on the settled box alike. */\nexport function sectionClass(section: PageSection): string {\n return (\n `page-section page-${section.kind} width-${section.width ?? (section.kind === 'metric' ? 'auto' : 'full')}` +\n `${section.offsetLeft ? ` offset-l-${section.offsetLeft}` : ''}` +\n `${section.offsetRight ? ` offset-r-${section.offsetRight}` : ''}`\n );\n}\n\n/** Where a page came from, which is the same question as who may change it. */\nexport function sourceOf(page: { source: string }): string {\n return page.source === 'saved'\n ? say('savedOnThisServer')\n : page.source === 'config'\n ? say('fromTheAppOrConnection')\n : say('builtFromTheSchema');\n}\n\n/** The page's record header, if it declares one, with the index its plan and label are keyed by. */\nexport function headerOf(page: PageDef): { section: PageSection; index: number } | undefined {\n const index = page.sections.findIndex((section) => section.kind === 'fields' && section.presentation === 'header' && !section.hidden);\n return index === -1 ? undefined : { section: page.sections[index], index };\n}\n","/**\n * Pages, as the vanilla client reaches them.\n *\n * The page is a component (components/Page.tsx) and the room around it —\n * which definition a link names, the root, the probe, the plan, the crumb —\n * is components/PageRoom.tsx. What stays here is the registry of pages every\n * other module asks: the config's, the suggested one every table has, and\n * the ones this browser built; and how a page is spelled in a link.\n */\nimport { api, findTable, state } from './core.js';\nimport { savedPages, whenSavedPagesChange } from './savedPages.js';\nimport { shell } from './shellMode.js';\nimport { appRequestScope } from './requestIdentity.js';\nimport { resourceOf } from './components/appRooms.js';\nimport { connectionGeneration } from './connectionTransition.js';\nimport { say } from './copy/record/page.js';\n\nexport { rootMatches } from './components/pageModel.js';\n\n/* ---------- lookup ---------- */\n\n/** @typedef {{connection: string | null | undefined, schema: import('@mistriwork/engine/shared/schema.js').DatabaseSchema | null, pages?: import('@mistriwork/engine/shared/page.js').PageDef[], pending?: Promise<import('@mistriwork/engine/shared/page.js').PageDef[]>}} PageCache */\n/** @type {PageCache | undefined} */\nlet cache;\n\n// Cache identity is also the load generation: invalidation retires every reader of it.\nfunction pageOwner() {\n if (!cache || cache.connection !== state.activeConnection || cache.schema !== state.schema) {\n cache = { connection: state.activeConnection, schema: state.schema };\n }\n return cache;\n}\n\n/** @param {PageCache} owner */\nfunction assertCurrent(owner) {\n if (cache !== owner || owner.connection !== state.activeConnection || owner.schema !== state.schema) {\n throw new Error(say('theConnectionOrPage'));\n }\n}\n\n/* The configured pages asked for at start beside the schema and the App,\n not after them: adopted by the first cache of the same connection in the\n same connection generation, and dropped with the cache otherwise. The\n answer is the server's for this connection either way; asking it a round\n trip earlier changes only when it arrives. */\n/** @type {{ connection: string | null | undefined, generation: number, promise: Promise<{ pages?: import('@mistriwork/engine/shared/page.js').PageDef[] }> } | undefined} */\nlet early;\nexport function prefetchPages() {\n const promise = api('/api/pages');\n promise.catch(() => {});\n early = { connection: state.activeConnection, generation: connectionGeneration(), promise };\n}\n\n/** @param {PageCache} owner */\nasync function configuredPages(owner) {\n assertCurrent(owner);\n if (owner.pages) return owner.pages;\n if (!owner.pending) {\n const adopted =\n early && early.connection === owner.connection && early.generation === connectionGeneration() ? early.promise : undefined;\n early = undefined;\n owner.pending = (adopted ?? api('/api/pages'))\n .then((data) => {\n assertCurrent(owner);\n /* A document is a page put on paper, reached from its record as a verb (documents.ts): never a\n page a record opens through, nor one offered beside the record's own. */\n return (owner.pages = (data.pages ?? []).filter((page) => page.document === undefined));\n })\n .finally(() => {\n owner.pending = undefined;\n });\n }\n return owner.pending;\n}\n\n// Saved layouts belong to the standalone browser, including while an App's\n// payload is loading. Never even read an App's stale/malformed local mirror.\nconst appPagesOnly = () => Boolean(shell().app()) || appRequestScope() !== false;\nconst browserPages = () => (appPagesOnly() ? [] : savedPages());\n\n/* App records use their authored page, or a suggestion only when none exists.\n The standalone browser retains its own saved-page precedence. The walk asks\n it through the shell, which names it at boot (app.js, `resolvePage`). */\n/** @param {string} tableId */\nexport async function recordPageId(tableId) {\n const pages = await pagesFor(tableId);\n const built = new Set(browserPages().map((p) => p.id));\n const named = resourceOf(shell().app(), tableId)?.page;\n const [first] = pages;\n const chosen = first && built.has(first.id) ? first : (named && pages.find((p) => p.id === named)) || first;\n if (appPagesOnly() && chosen?.error) throw new Error(chosen.error);\n return chosen && !chosen.error ? chosen.id : `suggested:${tableId}`;\n}\n\n/** Every page for this connection; standalone Workbench includes its saved pages first. */\nexport async function pagesFor(tableId) {\n const owner = pageOwner();\n const pages = await configuredPages(owner);\n assertCurrent(owner);\n const all = [...browserPages(), ...pages];\n return tableId ? all.filter((p) => p.base === tableId) : all;\n}\n\n/**\n * Told when the set of pages changes, so the menu can be rebuilt. A\n * callback rather than an import: the builder announces, and whoever cares\n * listens — an import the other way made a cycle.\n */\nlet listener = null;\n\nexport function whenPagesChange(fn) {\n listener = fn;\n}\n\nexport function forgetPages() {\n cache = undefined;\n early = undefined;\n listener?.();\n}\nwhenSavedPagesChange(forgetPages);\n\nexport async function pageById(id, tableId) {\n const owner = pageOwner();\n const suggested = async (base) => {\n assertCurrent(owner);\n const data = await api(`/api/pages/suggested?table=${encodeURIComponent(base)}`);\n assertCurrent(owner);\n return data.page;\n };\n const mine = browserPages().find((p) => p.id === id);\n if (mine) return mine;\n if (id?.startsWith('suggested:')) {\n const base = id.slice('suggested:'.length);\n if (appPagesOnly()) {\n const authored = await recordPageId(base);\n assertCurrent(owner);\n if (authored !== id) return pageById(authored, base);\n }\n return suggested(base);\n }\n const pages = await pagesFor();\n assertCurrent(owner);\n const found = pages.find((p) => p.id === id);\n if (found) return found;\n\n /* A link may name the page rather than identify it — `@customer-overview`\n rather than `@saved:customer:1787509869158`. Ids are tried first. */\n const named = pages.filter((p) => pageSlug(p) === id);\n const mineFirst = named.find((p) => !tableId || p.base === tableId) ?? named[0];\n if (mineFirst) return mineFirst;\n\n /* Legacy bare table links use the App's page, or the browser's suggestion. */\n const asTable = findTable(id);\n if (asTable) {\n if (appPagesOnly()) {\n const authored = await recordPageId(asTable.id);\n assertCurrent(owner);\n return pageById(authored, asTable.id);\n }\n return suggested(asTable.id);\n }\n if (tableId) {\n if (appPagesOnly()) throw new Error(say('thereIsNoPage', { id }));\n return suggested(tableId);\n }\n throw new Error(say('thereIsNoPage', { id }));\n}\n\n/**\n * A page's name as it is written in a link: the readable half of an id\n * that would otherwise carry a timestamp into every pasted ticket.\n */\nfunction pageSlug(page) {\n return String(page?.name ?? '')\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '');\n}\n\n/**\n * The token a page is written as in the hash: its slug where that names it\n * unambiguously, its id otherwise. Synchronous, because the address bar is\n * stamped during a render.\n */\nexport function pageToken(pageId) {\n if (!pageId) return pageId;\n if (pageId.startsWith('suggested:')) return pageId.slice('suggested:'.length);\n const known = [...browserPages(), ...(pageOwner().pages ?? [])];\n const page = known.find((p) => p.id === pageId);\n if (!page) return pageId;\n const slug = pageSlug(page);\n /* Ambiguity keeps the id: two pages called \"Overview\" would otherwise both\n answer to `@overview`. A table of the same name counts too. */\n const clashes = known.filter((p) => pageSlug(p) === slug).length > 1 || Boolean(findTable(slug));\n return slug && !clashes ? slug : pageId;\n}\n","/**\n * Counting things by something: GROUP BY, as a shape rather than as text.\n *\n * This was the last real hole in the query language. \"How many invoices per\n * month\", \"which countries have the most customers\", \"what does each status\n * add up to\" are the questions anybody asks of a database in the first ten\n * minutes, and until now the only way to ask them here was to drop into `sql`\n * — which means an agent leaves the checked path exactly where the question\n * gets interesting, and a person leaves the tool.\n *\n * A breakdown is deliberately *not* a view with a `groupBy` bolted on. A view\n * promises one row per base record, and every part of it — the correlated\n * aggregates, the LEFT JOIN rule, the row-click that opens a record — leans on\n * that promise. Grouping breaks it: the rows that come back are not records\n * and cannot be opened, and a column that is neither a key nor an aggregate is\n * a bug on two engines and a silent wrong answer on the third. So it is a\n * sibling type with its own compiler, sharing the parts that are genuinely the\n * same: path resolution, the join planner, and `buildWhere`.\n *\n * invoice count by status\n * invoice sum total by month invoice_date\n * invoice count, avg total by customer_id.country_code.name sort count desc\n *\n * Two rules the shape enforces, both of which SQL leaves to the author:\n *\n * - **Every selected expression is a key or a measure.** There is nowhere to\n * put a third thing, so `SELECT status, name, COUNT(*) … GROUP BY status`\n * — which a lenient engine would answer with an arbitrary `name` — cannot be\n * written.\n * - **The result is bounded like every other read.** A breakdown over a\n * high-cardinality column is a million-row answer, and `sort count desc\n * limit 20` is what somebody meant anyway.\n */\nimport { buildWhere, type Condition, type DateUnit, type Filter, type SqlStyle } from './filter.js';\nimport { clampLimit, clampOffset, countStatement } from './reads.js';\nimport { aggregateInput, columnKind, findTable, labelPath, type DatabaseSchema, type ForeignKey } from './schema.js';\nimport { JoinPlan, PathError, sqlLiteral, type ViewFilter } from './view.js';\nimport { ScopedRelations, type RowScope } from './rowScope.js';\nimport { readsOf } from './query.js';\nimport { isFixedScopeCatalog } from './scopeCatalog.js';\nimport { viewPaths, withViewComputed } from './viewLabels.js';\nimport { tableAtPrefix, tryResolvePath, type ViewDef } from './viewPaths.js';\nimport type { ComputedPath } from './rollups.js';\nimport { withViewReferences } from './declaredReferences.js';\nimport { declaredScale, exactSqliteSum, sumsExactly } from './decimals.js';\nimport { LABEL_SEGMENT, labelPrefix } from './recordLabel.js';\n\n/** What a measure can do. `count` is the only one that needs no column. `median` is PostgreSQL's\n `percentile_cont(0.5)`: SQLite has no median aggregate, so it is refused there. */\nexport type MeasureFn = 'count' | 'sum' | 'avg' | 'min' | 'max' | 'median';\n\nexport const MEASURE_FUNCTIONS: MeasureFn[] = ['count', 'sum', 'avg', 'min', 'max', 'median'];\n\n/** The arithmetic a computed measure joins its terms with. SQL's precedence: `*` and `/` before `+` and `-`. */\nexport type MeasureOp = '+' | '-' | '*' | '/';\nexport const MEASURE_OPS: readonly MeasureOp[] = ['+', '-', '*', '/'];\n\n/** Why a dialect cannot answer a measure function, or undefined when it can. */\nexport function measureRefusal(fn: MeasureFn, dialect: string | undefined): string | undefined {\n if (fn !== 'median') return undefined;\n if (dialect === 'sqlite')\n return '`median` is not available on SQLite: it has no median aggregate (no percentile_cont). Use `avg`, or a PostgreSQL source.';\n return undefined;\n}\n\n/** The periods a date key can be cut into. */\nexport const DATE_UNITS: DateUnit[] = ['day', 'week', 'month', 'quarter', 'year', 'weekday', 'hour'];\n\n/**\n * One thing to group by: a path, optionally cut down to a period.\n *\n * The path is a reference path like a view's, so \"customers by country name\"\n * is `customer … by country_code.name` and needs no join written by hand.\n */\nexport interface BreakdownKey {\n path: string;\n /** Cut a date or timestamp down to a period. Refused on anything else. */\n bucket?: DateUnit;\n alias?: string;\n}\n\n/** One aggregate: a function and the column it reads. */\nexport interface MeasureTerm {\n fn: MeasureFn;\n /** The column to measure. Absent means `COUNT(*)`, and is only valid there. */\n path?: string;\n /** COUNT DISTINCT. Meaningless on the others and refused there. */\n distinct?: boolean;\n /**\n * `count when [status = open]`: only the rows that hold these conditions are counted, in the same statement as the\n * other measures (`COUNT(CASE WHEN … THEN 1 END)`), so a funnel's stages are one snapshot and can never disagree.\n * Only a bare `count` takes it. `when` is the conditions as written; `where` is what they parsed to, which is what\n * runs: the parser fills it, and a request's own is held to the same shape a filter is (resourceReadOperations.ts).\n */\n when?: string;\n where?: ViewFilter;\n}\n\n/**\n * A measure: one aggregate, or a computed measure — aggregates and numbers\n * joined by arithmetic, `sum paid / sum total`, `sum total - sum cost`. A\n * ratio is the `/` case, not a function of its own. A division never\n * truncates and never fails: its divisor is read as a decimal and a zero\n * one answers null.\n */\nexport interface Measure extends MeasureTerm {\n alias?: string;\n /** The terms after the first, each with the operator before it. */\n steps?: { op: MeasureOp; term: MeasureTerm | number }[];\n}\n\nconst termText = (t: MeasureTerm | number): string =>\n typeof t === 'number'\n ? String(t)\n : [t.fn, ...(t.distinct ? ['distinct'] : []), ...(t.path ? [t.path] : []), ...(t.when ? ['when', `[${t.when}]`] : [])].join(' ');\n\n/** A measure as it is written: `sum paid / sum total as rate`. */\nexport function measureToText(m: Measure): string {\n return [termText(m), ...(m.steps ?? []).flatMap(({ op, term }) => [op, termText(term)]), ...(m.alias ? ['as', m.alias] : [])].join(' ');\n}\n\nconst termName = (t: MeasureTerm | number, path = typeof t === 'number' ? undefined : t.path): string =>\n typeof t === 'number' ? String(t) : t.fn === 'count' ? (path ? `count(${path})` : 'count') : `${t.fn}(${path ?? ''})`;\n\n/** The name a measure answers under when it has no alias: `sum(total)`, `sum(paid) / sum(total)`. */\nexport function measureName(m: Measure, path = m.path): string {\n return m.alias?.trim() || [termName(m, path), ...(m.steps ?? []).flatMap(({ op, term }) => [op, termName(term)])].join(' ');\n}\n\n/** Every column a measure reads. */\nexport function measurePaths(m: Measure): string[] {\n return [m, ...(m.steps ?? []).map(({ term }) => term)].flatMap((t) => (typeof t !== 'number' && t.path ? [t.path] : []));\n}\n\n/** Every `when` among a breakdown's measures, in order: what a reader's grants and the stitched-read check must also hold. */\nexport const measureConditions = (measures: readonly Measure[] | undefined): ViewFilter[] =>\n (measures ?? []).flatMap((m) => (m.where?.groups.length ? [m.where] : []));\n\n/** The paths a filter's conditions walk, and the paths they compare against. */\nexport const filterPaths = (filter: ViewFilter | undefined): string[] =>\n (filter?.groups ?? []).flatMap((group) => group.flatMap((c) => [c.path, ...(c.other === undefined ? [] : [c.other])]));\n\nexport interface BreakdownOrder {\n /** A key's or a measure's output name. Breakdowns sort by what they show. */\n name: string;\n direction: 'asc' | 'desc';\n /** On a key: this entry only moves the empty group (a null key) after every other, whatever the entries after it\n say; it orders nothing else, so `direction` is not read. The groups of a grouped list use it to draw \"No sprint\"\n last under any sort — done in the query because the groups are paged, so no page could place it by itself. */\n emptyLast?: boolean;\n}\n\nexport interface BreakdownDef {\n base: string;\n by: BreakdownKey[];\n /** `table.column` of text columns an App declares dates (declaredDates.ts), which this catalog reads as TEXT:\n a period bucket over one is allowed. Carried by the App's resource facade, never taken from a request. */\n dateColumns?: string[];\n measures: Measure[];\n filter?: ViewFilter;\n orderBy?: BreakdownOrder[];\n limit?: number;\n /** The breakdown written down, when it came from text. */\n text?: string;\n /** The ISO weekday a `week` bucket's weeks start on, 1 (Monday) to 7 (Sunday); Monday when unsaid. An App's\n server sets it from `ui.locale` (weekStart.ts), whatever a request carried. */\n weekStart?: number;\n /**\n * The rollups its paths name (rollups.ts), by path — a measure `sum\n * on_hand`, a key, a condition. Filled by the server from the App's\n * catalog (withBreakdownComputed), never taken from a request, and read\n * by the compiler where the adapter's catalog does not carry them.\n */\n computed?: Record<string, ComputedPath>;\n /** The computed columns its paths name (computed.ts), pinned the same way. */\n expressions?: ViewDef['expressions'];\n /** The record tags its conditions ask (`tags has vpn`, tagLists.ts), pinned the same way: a filter rail's\n counts under a tags filter ask them where the adapter's catalog has no tags. */\n tagged?: ViewDef['tagged'];\n /** The App's declared references it may walk, as a view carries them (ViewDef.references); never a request's. */\n references?: ForeignKey[];\n /**\n * The keys that are references, each with the path of the name of the\n * record it points at (`customer_id` → `customer_id.name`): a group of\n * `customer_id` is a customer, and is said by the customer's name, as a\n * grid's cell says it. Each such key answers that name in a `label` column\n * after the measures. Filled by the server from the catalog\n * (withBreakdownComputed), never taken from a request, as are `labels`,\n * the record-name templates a `$label` name needs (a view's `labels`).\n */\n names?: Record<string, string>;\n labels?: Record<string, string>;\n}\n\nexport interface CompiledMeasure {\n name: string;\n /** A key, a measure, or a key's record name (`of` names the key). */\n kind: 'key' | 'measure' | 'label';\n of?: string;\n fn?: MeasureFn;\n /** The path this came from, for the interface to label it with. */\n path?: string;\n /** What the values are, as far as the schema knows. */\n type: string;\n bucket?: DateUnit;\n}\n\ninterface CompiledBreakdown {\n text: string;\n params: unknown[];\n columns: CompiledMeasure[];\n limit: number;\n}\n\n/** How many group keys are worth having. */\nconst MAX_KEYS = 3;\n\n/**\n * Is this column something a date bucket can be taken of?\n *\n * Read off the declared type rather than the values, because the answer has\n * to be the same before and after the table has any rows in it. SQLite's\n * `DATE` is a convention rather than a type, which is exactly why the check is\n * on the declaration: a column somebody called `DATE` is one they intend to\n * hold dates, and `strftime` over text that is not one returns null — a group\n * of its own rather than an error, which is the honest outcome.\n */\nconst isDateish = (type: string): boolean => /date|time/i.test(type ?? '');\n\n/**\n * Compile a breakdown to one parameterised SELECT.\n *\n * The order of work matches `compileView`'s and for the same reason: keys,\n * then measures, then the filter, then the sort — so aliases are handed out in\n * first-use order and the same breakdown always compiles to the same text.\n */\nexport function compileBreakdown(\n schema: DatabaseSchema,\n def: BreakdownDef,\n style: SqlStyle,\n offset: number | unknown = 0,\n scope?: RowScope,\n): CompiledBreakdown {\n /* A breakdown compiles to the same statement against the same catalog, and an App's enter asks for each of its\n rail's and dashboard's breakdowns several times — against the reader's pruned catalog and the full one for\n the access check (columnGrants.ts), under the reader's scope to run it, and again for the SQL pane — each a\n full compile (a field report: a large tenant App spent a twelfth of an enter's CPU here). Kept only against a catalog nothing can change (fixedCatalog, below), for a definition\n whose text says all of it (breakdownText), and only where compiling read neither the clock nor the reader\n (query.ts readsOf): a statement that wrote a request's instant, day or email is that request's, and is never\n handed to another. */\n const text = typeof offset === 'number' && fixedCatalog(schema) ? breakdownText(def) : undefined;\n if (text === undefined) {\n breakdownsCompiled++;\n return compileBreakdownFresh(schema, def, style, offset, scope);\n }\n let byStyle = breakdownsKept.get(schema);\n if (!byStyle) breakdownsKept.set(schema, (byStyle = new WeakMap()));\n let kept = byStyle.get(style);\n if (!kept) byStyle.set(style, (kept = new Map()));\n const key = `${scope ? scopeOrdinal(scope) : 0}\\0${offset as number}\\0${text}`;\n let found = kept.get(key);\n if (found) {\n kept.delete(key);\n kept.set(key, found);\n } else {\n breakdownsCompiled++;\n const read = readsOf(() => compileBreakdownFresh(schema, def, style, offset, scope));\n if (read.clock !== 0 || read.reader) return read.value;\n found = read.value;\n kept.set(key, found);\n if (kept.size > BREAKDOWNS_KEPT) kept.delete(kept.keys().next().value!);\n }\n // Each caller's own arrays: a driver or a caller may add to what it is handed.\n return { ...found, params: [...found.params], columns: found.columns.map((column) => ({ ...column })) };\n}\n/* A catalog nothing can change: a captured scope catalog (scopeCatalog.ts), or whatever the server says it froze\n all the way down (evidence.ts captureTenantData, which registers itself here: a shared module cannot import it). */\nlet alsoFixed: (schema: DatabaseSchema) => boolean = () => false;\n/** Who says a catalog cannot change, beside the scope catalog's own captures. */\nexport function keepBreakdownsAgainst(fixed: (schema: DatabaseSchema) => boolean): void {\n alsoFixed = fixed;\n}\nconst fixedCatalog = (schema: DatabaseSchema) => isFixedScopeCatalog(schema) || alsoFixed(schema);\n\n/* A definition's text, where the text says all of it: plain records, arrays, strings, booleans and finite numbers.\n A property left undefined (a key with no `bucket`, a list with no `orderBy` — most of them, as a reader's snapshot\n carries them) is said as one, never dropped, so the text of a definition that has the key and one that does not\n still differ; anything else JSON would say alike — an undefined in a list, a non-finite number, an object that is\n not a plain record, a function, a symbol or a BigInt — is no text, and is compiled afresh. */\nconst UNDEFINED = Object.freeze({ '\\u0000': 'undefined' });\nfunction breakdownText(def: BreakdownDef): string | undefined {\n let plain = true;\n const text = JSON.stringify(def, function (this: Record<string, unknown>, key, item: unknown) {\n // The value as held, not as its toJSON says it: a Date is said as text, and is not the text it says.\n const held = this[key];\n if (held === undefined) {\n if (Array.isArray(this)) plain = false;\n return UNDEFINED;\n }\n // JSON drops a function or symbol (two definitions apart only by one would share a text) and throws on a BigInt.\n if (typeof held === 'function' || typeof held === 'symbol' || typeof held === 'bigint') {\n plain = false;\n return null;\n }\n if (\n (typeof held === 'number' && !Number.isFinite(held)) ||\n (held !== null && typeof held === 'object' && !Array.isArray(held) && ![null, Object.prototype].includes(Object.getPrototypeOf(held)))\n )\n plain = false;\n return item;\n });\n return plain ? text : undefined;\n}\n\nconst breakdownsKept = new WeakMap<DatabaseSchema, WeakMap<SqlStyle, Map<string, CompiledBreakdown>>>();\nconst BREAKDOWNS_KEPT = 256;\nlet breakdownsCompiled = 0;\n/** For tests: how many breakdowns were compiled rather than found. */\nexport function breakdownsCompiledSoFar(): number {\n return breakdownsCompiled;\n}\nconst scopeOrdinals = new WeakMap<RowScope, number>();\nlet scopesSeen = 0;\nconst scopeOrdinal = (scope: RowScope): number => {\n let ordinal = scopeOrdinals.get(scope);\n if (ordinal === undefined) scopeOrdinals.set(scope, (ordinal = ++scopesSeen));\n return ordinal;\n};\n\nfunction compileBreakdownFresh(\n schema: DatabaseSchema,\n def: BreakdownDef,\n style: SqlStyle,\n offset: number | unknown,\n scope: RowScope | undefined,\n): CompiledBreakdown {\n // The App's soft keys, where this catalog is the database's own (declaredReferences.ts).\n schema = withViewReferences(schema, def.references);\n const base = findTable(schema, def.base);\n if (!base) {\n throw new PathError(`No table called \"${def.base}\" in this database, so there is nothing to break down.`, def.base, def.base, 0);\n }\n /* No `by` is one group: the whole answer as a single row —\n \"invoice sum total\" is a number, not a mistake. The app home's metric\n cards ride on exactly this, and it reads the same in the workbench. */\n if (def.by.length > MAX_KEYS) {\n /* A cap rather than a warning. Three keys is already a table nobody reads\n across; the fourth is how a breakdown becomes the base table again, one\n row per record, with the count column always saying 1. */\n throw new PathError(\n `A breakdown groups by at most ${MAX_KEYS} things; this one names ${def.by.length}.`,\n def.by[MAX_KEYS].path,\n def.by[MAX_KEYS].path,\n 0,\n );\n }\n if (!def.measures?.length) {\n throw new PathError('A breakdown needs something to measure — `count`, or `sum total`.', '', '', 0);\n }\n\n const relations = new ScopedRelations(schema, style, scope);\n const baseRelation = relations.table(base.id);\n const plan = new JoinPlan(schema, base, style, relations.table);\n /* The view compiler's paths, so a rollup — the base's or through a\n reference — is the same correlated subquery here as in a list, its\n child rows read through this statement's relations (a row scope's\n CTE). A key, a measure and a condition may each name one. */\n const paths = viewPaths(\n schema,\n {\n id: '',\n name: '',\n base: def.base,\n ...(def.computed ? { computed: def.computed } : {}),\n ...(def.expressions ? { expressions: def.expressions } : {}),\n ...(def.tagged ? { tagged: def.tagged } : {}),\n ...(def.labels ? { labels: def.labels } : {}),\n } as ViewDef,\n plan,\n style,\n relations,\n );\n const qualify = (path: string): string => paths.qualify(path);\n const membership = paths.membership;\n const exists = paths.exists;\n\n const seen = new Map<string, number>();\n /** Output names are unique or the result set loses a column silently. */\n const nameFor = (wanted: string): string => {\n const count = seen.get(wanted) ?? 0;\n seen.set(wanted, count + 1);\n return count ? `${wanted}_${count + 1}` : wanted;\n };\n\n const columns: CompiledMeasure[] = [];\n const select: string[] = [];\n const grouping: string[] = [];\n\n for (const key of def.by ?? []) {\n const resolved = paths.resolve(key.path);\n let sql = qualify(key.path);\n if (key.bucket) {\n // A request's word, looked up in the dialect's own table: one of the four, never anything else.\n if (!DATE_UNITS.includes(key.bucket)) {\n throw new PathError(\n `\"${String(key.bucket).slice(0, 40)}\" is not a period. Use one of: ${DATE_UNITS.join(', ')}.`,\n key.path,\n key.path,\n 0,\n );\n }\n if (!isDateish(resolved.column.type) && !def.dateColumns?.includes(`${resolved.table.id}.${resolved.column.name}`)) {\n throw new PathError(\n `\"${key.path}\" is ${resolved.column.type || 'untyped'}, so it cannot be grouped by ${key.bucket}. ` +\n 'Group by it directly, or pick a date column.',\n key.path,\n key.path,\n resolved.hops.length,\n );\n }\n sql = style.dateBucket(sql, key.bucket, def.weekStart);\n }\n\n const name = nameFor(key.alias?.trim() || (key.bucket ? `${key.bucket}(${resolved.path})` : resolved.path));\n select.push(`${sql} AS ${style.quote(name)}`);\n /* Grouped by the *expression*, not by the output name. Postgres\n would take the alias; SQLite would too — but a bucketed key aliased to\n `month(invoice_date)` and grouped by that name is one identifier away\n from grouping by a column that happens to share it. The expression is\n never ambiguous.\n\n A rollup is the exception: its expression is a correlated subquery,\n and an engine can fail to see two copies of one as\n the same thing, so it refuses the SELECT list's copy as ungrouped.\n Grouped by its position in the SELECT list instead — the one place it\n is written, which every engine groups by and nothing can shadow. */\n grouping.push(def.computed && Object.hasOwn(def.computed, key.path) ? String(select.length) : sql);\n columns.push({\n name,\n kind: 'key',\n path: resolved.path,\n /* A bucket makes text out of whatever it was, and the interface should\n render `2026-08` as the label it is rather than as a date it is not. */\n type: key.bucket ? 'text' : resolved.column.type,\n bucket: key.bucket,\n });\n }\n\n /* A measure said as text whose order is a number: an exact SQLite sum, by its output name. */\n const orderedBy = new Map<string, string>();\n /* One aggregate's SQL, and what its values are. `exact` asks for a decimal column's sum as exact text where the\n engine would add floats (decimals.ts): a measure on its own, never a term of arithmetic, which reads a number. */\n const termSql = (term: MeasureTerm, exact = false): { sql: string; path?: string; type: string; order?: string } => {\n /* The function is written into the statement as a word, so it is one of\n the words: a structured def arrives from a request (`/api/breakdown`\n `def`), and anything else there would be SQL of the caller's own,\n outside every scope and grant this statement reads through. */\n if (typeof term !== 'object' || term === null || !MEASURE_FUNCTIONS.includes(term.fn)) {\n throw new PathError(`\"${String(term?.fn).slice(0, 40)}\" is not a measure. Use one of: ${MEASURE_FUNCTIONS.join(', ')}.`, '', '', 0);\n }\n if (term.fn === 'count') {\n if (term.distinct && !term.path) {\n throw new PathError('`count distinct` needs a column to count distinct values of.', '', '', 0);\n }\n /* COUNT(*) rather than COUNT(column) when no column is named: a column\n count skips nulls, which turns \"how many invoices\" into \"how many\n invoices have a total\" without saying so. Naming a column is how you\n ask the other question, and then the skipping is the point. */\n return {\n sql: term.path ? `COUNT(${term.distinct ? 'DISTINCT ' : ''}${qualify(term.path)})` : 'COUNT(*)',\n path: term.path,\n type: 'integer',\n };\n }\n if (!term.path) {\n throw new PathError(`\\`${term.fn}\\` needs a column: \\`${term.fn} total\\`.`, '', '', 0);\n }\n if (term.distinct) {\n throw new PathError(`\\`distinct\\` only means something with \\`count\\`, not with \\`${term.fn}\\`.`, term.path, term.path, 0);\n }\n const refused = measureRefusal(term.fn, style.dialect);\n if (refused) throw new PathError(refused, term.path, term.path, 0);\n const resolved = paths.resolve(term.path);\n const read = aggregateInput(qualify(term.path), resolved.column.type, style.dialect);\n /* SQLite adds a decimal column as floats, a cent off at large totals: summed in its smallest unit instead, and\n said with its places, as PostgreSQL's exact numeric says it. */\n if (exact && term.fn === 'sum' && style.dialect === 'sqlite' && sumsExactly(resolved.column.type)) {\n const sum = exactSqliteSum(read, declaredScale(resolved.column.type), resolved.path);\n return { sql: sum.text, path: resolved.path, type: resolved.column.type, order: sum.total };\n }\n return {\n sql: term.fn === 'median' ? `percentile_cont(0.5) WITHIN GROUP (ORDER BY ${read})` : `${term.fn.toUpperCase()}(${read})`,\n path: resolved.path,\n /* `avg` and `median` are fractions of whatever they read — an average\n of integers is not an integer, and saying otherwise makes the\n interface round a number the database did not. */\n type:\n term.fn === 'avg' || term.fn === 'median'\n ? 'numeric'\n : term.fn === 'sum' && columnKind(resolved.column.type) === 'boolean'\n ? 'integer'\n : resolved.column.type,\n };\n };\n\n /* A measure that counts only some rows (`count when [...]`) is written once the statement's own parameters are\n counted, below: its values are bound after the scope's and before the filter's, in the order the text reads. */\n const whens: { at: number; name: string; where: ViewFilter }[] = [];\n const asFilter = (filter: ViewFilter | undefined): Filter | undefined =>\n filter?.groups.length\n ? {\n groups: filter.groups.map((group) =>\n group.map((c): Condition =>\n c.any\n ? { column: c.path, op: c.op, any: c.any }\n : c.other === undefined\n ? { column: c.path, op: c.op, value: c.value }\n : { column: c.path, op: c.op, other: c.other },\n ),\n ),\n }\n : undefined;\n /* Every path a condition names is planned (its joins, its tags) before the scope prefix is frozen. */\n const planPaths = (conditions: Filter | undefined) => {\n for (const group of conditions?.groups ?? []) {\n for (const condition of group) {\n /* A tags condition (`has`) plans its link and tag relations here too, before the scope\n prefix is frozen, as a view's does: its EXISTS reads them through it. */\n if (condition.any) {\n exists(condition.any);\n continue;\n }\n qualify(condition.column);\n if (condition.other !== undefined) qualify(condition.other);\n }\n }\n };\n\n for (const measure of def.measures) {\n if (measure.where !== undefined || measure.when !== undefined) {\n if (measure.fn !== 'count' || measure.path || measure.distinct || measure.steps?.length || !measure.where) {\n throw new PathError('`when` only goes with a bare `count`: `count when [status = open]`.', '', '', 0);\n }\n const conditions = asFilter(measure.where);\n if (conditions) {\n planPaths(conditions);\n const name = nameFor(measureName(measure, undefined));\n whens.push({ at: select.length, name, where: measure.where });\n select.push('');\n columns.push({ name, kind: 'measure', fn: 'count', type: 'integer' });\n continue;\n }\n }\n const head = termSql(measure, !measure.steps?.length);\n if (!measure.steps?.length) {\n const name = nameFor(measureName(measure, head.path));\n select.push(`${head.sql} AS ${style.quote(name)}`);\n if (head.order) orderedBy.set(name, head.order);\n columns.push({ name, kind: 'measure', fn: measure.fn, path: head.path, type: head.type });\n continue;\n }\n /* A computed measure is written out in its own order: SQL's precedence\n is the grammar's. A divisor is made a decimal, so integer counts do\n not divide to zero on PostgreSQL and SQLite, and NULLIF makes a zero\n one an empty answer instead of an error. */\n const pieces = [head.sql];\n for (const { op, term } of measure.steps) {\n if (!MEASURE_OPS.includes(op)) throw new PathError(`\"${op}\" is not arithmetic a measure joins with: + - * or /.`, '', '', 0);\n if (typeof term === 'number' && !Number.isFinite(term)) throw new PathError(\"A computed measure's number must be finite.\", '', '', 0);\n const sql = typeof term === 'number' ? String(term) : termSql(term).sql;\n pieces.push(op, op === '/' ? `NULLIF(1.0 * ${sql}, 0)` : sql);\n }\n const name = nameFor(measureName(measure, head.path));\n select.push(`(${pieces.join(' ')}) AS ${style.quote(name)}`);\n columns.push({ name, kind: 'measure', type: 'numeric' });\n }\n\n /* A key that is a reference is said by its record's name, as a grid's cell\n is: the name through the same join the key's path walks, read through\n this statement's relations, so a record this reader's scope does not\n admit has no name and the group stays its key. Last, after the\n measures, so a reader that takes the key and the measure by position\n reads what it always did; grouped by position, since the key decides it. */\n for (const [index, key] of (def.by ?? []).entries()) {\n if (key.bucket || !def.names || !Object.hasOwn(def.names, key.path)) continue;\n const path = def.names[key.path];\n const name = nameFor(`${columns[index].name}.${LABEL_SEGMENT}`);\n select.push(`${qualify(path)} AS ${style.quote(name)}`);\n grouping.push(String(select.length));\n columns.push({ name, kind: 'label', of: columns[index].name, path, type: 'text' });\n }\n\n const conditions = asFilter(def.filter);\n planPaths(conditions);\n const joins = plan.clauses();\n const prefix = relations.finish();\n const whenParams: unknown[] = [];\n for (const { at, name, where: held } of whens) {\n const built = buildWhere(\n asFilter(held),\n style,\n prefix.params.length + whenParams.length + 1,\n qualify,\n qualify,\n membership,\n exists,\n false,\n paths.typeOf,\n );\n whenParams.push(...built.params);\n /* COUNT, not SUM: a count of no rows is 0, where a sum of none is null. */\n select[at] = `COUNT(CASE WHEN ${built.text.replace(/^ WHERE /, '')} THEN 1 END) AS ${style.quote(name)}`;\n }\n const where = buildWhere(\n conditions,\n style,\n prefix.params.length + whenParams.length + 1,\n qualify,\n qualify,\n membership,\n exists,\n false,\n paths.typeOf,\n );\n\n /* Sorted by output name, and only by output name.\n\n Every column of a breakdown is either a key or a measure, and both are in\n the SELECT list under a name — so there is nothing else it could sort by,\n and ORDER BY runs late enough to see the aliases on all three engines.\n Sorting by an expression instead would let `sort total desc` mean the\n ungrouped column on a statement where that column no longer exists. */\n const names = new Set(columns.map((c) => c.name));\n const order = (def.orderBy ?? []).map((o) => {\n if (!names.has(o.name)) {\n throw new PathError(`Nothing called \"${o.name}\" is in this breakdown. It shows: ${[...names].join(', ')}.`, o.name, o.name, 0);\n }\n if (o.emptyLast) return `(${style.quote(o.name)} IS NULL) ASC`;\n /* An exact sum is text; it sorts by the integer it says, never as text (\"9.00\" after \"10.00\"). */\n return `${orderedBy.get(o.name) ?? style.quote(o.name)} ${o.direction === 'desc' ? 'DESC' : 'ASC'}`;\n });\n\n const limit = clampLimit(def.limit);\n const skip = clampOffset(offset);\n const n = prefix.params.length + whenParams.length + where.params.length;\n\n const lines = [`SELECT ${select.join(',\\n ')}`, ` FROM ${baseRelation} ${style.quote(plan.baseAlias)}`, ...joins];\n if (where.text) lines.push(where.text);\n if (grouping.length) lines.push(` GROUP BY ${grouping.join(', ')}`);\n if (order.length) lines.push(` ORDER BY ${order.join(', ')}`);\n lines.push(` LIMIT ${style.placeholder(n + 1)} OFFSET ${style.placeholder(n + 2)}`);\n\n return { text: prefix.text + lines.join('\\n'), params: [...prefix.params, ...whenParams, ...where.params, limit, skip], columns, limit };\n}\n\n/**\n * The breakdown with the rollups its paths name, read from a catalog that\n * serves them (an App's) — and with any it arrived with dropped, as\n * withViewComputed does for a view: which rows a number sums is the\n * catalog's to say, never a request's.\n */\nexport function withBreakdownComputed(schema: DatabaseSchema, def: BreakdownDef): BreakdownDef {\n // A request's declared dates and names are dropped with its rollups: all are the catalog's to carry (appResources.ts).\n const { computed: _given, expressions: _written, tagged: _tagged, dateColumns: _dated, names: _names, labels: _labels, ...rest } = def;\n /* A key that is a reference to a record with a name: its groups are said\n by that name (compileBreakdown), the one a grid's cell reads (labelPath),\n through the same reference. */\n const names: Record<string, string> = {};\n const labels: Record<string, string> = {};\n for (const key of def.by ?? []) {\n if (typeof key?.path !== 'string' || key.bucket || Object.hasOwn(names, key.path)) continue;\n const target = tableAtPrefix(schema, def.base, key.path);\n const resolved = target && tryResolvePath(schema, def.base, key.path).resolved;\n if (!target || !resolved || resolved.rollup) continue;\n const naming = labelPath(schema, target);\n if (!naming) continue;\n names[key.path] = `${key.path}.${naming}`;\n const prefix = labelPrefix(names[key.path]);\n if (prefix !== undefined && target.labelTemplate) labels[prefix] = target.labelTemplate;\n }\n const named = Object.keys(names).length ? { ...rest, names, ...(Object.keys(labels).length ? { labels } : {}) } : rest;\n const paths = [\n ...(def.by ?? []).map((key) => key.path),\n ...(def.measures ?? []).flatMap(measurePaths),\n ...filterPaths(def.filter),\n ...measureConditions(def.measures).flatMap(filterPaths),\n ];\n /* The rollups and computed columns its paths name, as a view's are (withViewComputed). */\n const pinned = withViewComputed(schema, {\n id: '',\n name: '',\n base: def.base,\n columns: paths.filter((path): path is string => typeof path === 'string').map((path) => ({ path })),\n });\n return {\n ...named,\n ...(pinned.computed ? { computed: pinned.computed } : {}),\n ...(pinned.expressions ? { expressions: pinned.expressions } : {}),\n ...(pinned.tagged ? { tagged: pinned.tagged } : {}),\n };\n}\n\n/**\n * How many groups there are, ignoring the page.\n *\n * `SELECT COUNT(*) FROM (the grouped statement)` — the only honest way to\n * count groups, because the number is a property of the grouping rather than\n * of the base table. Without the limit and offset, which is the whole point of\n * asking.\n */\nexport function compileBreakdownCount(\n schema: DatabaseSchema,\n def: BreakdownDef,\n style: SqlStyle,\n scope?: RowScope,\n limit?: number,\n): { text: string; params: unknown[] } {\n const inner = compileBreakdown(schema, def, style, 0, scope);\n /* The LIMIT/OFFSET line is the last one and carries the last two\n parameters. Dropped by rebuilding rather than by string surgery would be\n nicer; it is the last line by construction, and the test says so. */\n const lines = inner.text.split('\\n');\n const body = lines.slice(0, -1).join('\\n');\n return {\n /* With a bound, at most `limit` groups are counted (countStatement). The\n grouping itself still reads what it groups; the bound keeps the count's\n answer, not the scan, from growing with the data. */\n text: limit === undefined ? `SELECT COUNT(*) AS n FROM (\\n${body}\\n) AS grouped` : countStatement(`(\\n${body}\\n) AS grouped`, limit),\n params: inner.params.slice(0, -2),\n };\n}\n\n/**\n * Render a breakdown as the SQL it will become, values inline.\n *\n * The exact counterpart of `explainView`, and here for the same reason: the\n * SQL pane is how somebody learns what a line of the language did on their\n * behalf, and a statement full of `?` teaches nothing about the filter that\n * produced it. It also has to be a statement that runs, because the pane\n * offers to open it in the SQL editor.\n */\nexport function explainBreakdown(schema: DatabaseSchema, def: BreakdownDef, style: SqlStyle, offset: number | unknown = 0): string {\n const compiled = compileBreakdown(schema, def, style, offset);\n let i = 0;\n return compiled.text.replace(/\\$\\d+|\\?/g, () => sqlLiteral(compiled.params[i++]));\n}\n\n/** A breakdown's groups said by name: key column → the key's value as text → the record's name. */\nexport type GroupNames = Record<string, Record<string, string>>;\n\n/**\n * The answer as its readers take it — the keys first and the measures after,\n * by position — with each reference key's names (compileBreakdown's `label`\n * columns) lifted out beside it: a chart reads its key and measure by index,\n * a board its count last, and a name among them would be read as either.\n */\nexport function withGroupNames<R extends { columns: string[]; rows: Record<string, unknown>[]; resolved: CompiledMeasure[] }>(\n result: R,\n): R & { names?: GroupNames } {\n const labels = result.resolved.filter((column) => column.kind === 'label' && column.of);\n if (!labels.length) return result;\n const names: GroupNames = {};\n for (const label of labels) {\n const said: Record<string, string> = {};\n for (const row of result.rows) {\n const key = row[label.of!],\n name = row[label.name];\n if (key !== null && key !== undefined && typeof name === 'string' && name !== '') said[String(key)] = name;\n }\n names[label.of!] = said;\n }\n const hidden = new Set(labels.map((label) => label.name));\n return {\n ...result,\n columns: result.columns.filter((name) => !hidden.has(name)),\n resolved: result.resolved.filter((column) => !hidden.has(column.name)),\n rows: result.rows.map((row) => Object.fromEntries(Object.entries(row).filter(([name]) => !hidden.has(name)))),\n names,\n };\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"shared-CMAg58rY.js","names":["say"],"sources":["../../../kit/src/client/copy/record/components/WriteConfirm.ts","../../../kit/src/client/copy/record/write.ts","../../../kit/src/client/write.js"],"sourcesContent":["/** What WriteConfirm says, in the record page room (packages/kit/src/client/messages.ts says how the catalog is split). */\nimport { defineMessages } from '../../../messages.js';\n\nexport const say = defineMessages({\n enableWriteMode: 'Enable write mode?',\n enableEditingOfExisting: 'Enable editing of existing records in',\n fieldChangesStayPending: 'Field changes stay pending until you review and apply them together.',\n theReviewNamesTheRecord: 'The review names the record, changed values, and SQL statement.',\n anUpdateThatWould: 'An update that would touch more than one row is refused.',\n writeModeStaysOnForThisDatabase: 'Write mode stays on for this database until you turn it off, including after a reload.',\n itAppliesToThisDatabase: 'It applies to this database only — switching connections does not carry it over.',\n keepViewing: 'Keep viewing',\n enableWriteModeButton: 'Enable write mode',\n});\n","/** What write says, in the record page room (packages/kit/src/client/messages.ts says how the catalog is split). */\nimport { defineMessages } from '../../messages.js';\n\nexport const say = defineMessages({\n resolveThePendingOrUnconfirmed: 'Resolve the pending or unconfirmed change before leaving this view.',\n waitForThePending: 'Wait for the pending record update before changing modes.',\n discardPendingRecordChanges: 'Discard pending record changes?',\n theseFieldChangesHaveNotBeen: 'These field changes have not been applied.',\n discardChanges: 'Discard changes',\n thisConnectionIsRead: 'This connection is read-only.',\n unsavedChangesWereRetained: 'Unsaved changes were retained in this tab. Review them when you return.',\n theRecordConnectionOrTable: 'The record, connection or table definition changed. Review the current record before applying changes.',\n theRecordChangedBefore: 'The record changed before saving. Review it and save again.',\n updatedColumnsOnTheOriginal: {\n one: 'Updated {count} column on the original record in {connection}.',\n other: 'Updated {count} columns on the original record in {connection}.',\n },\n changesSaved: 'Changes saved',\n updatedColumns: { one: 'Updated {count} column on {name}.', other: 'Updated {count} columns on {name}.' },\n applyThisChange: 'Apply this change?',\n applyChanges: { one: 'Apply {count} change?', other: 'Apply {count} changes?' },\n unknownTable: 'Unknown table.',\n thisIsAView: 'This is a view. Views are read-only.',\n thisTableHasNoPrimary: 'This table has no primary key, so there is no way to identify one row safely.',\n parameters: 'parameters {params}',\n theOriginalConnection: 'the original connection',\n});\n","/**\n * Write mode.\n *\n * Workbench defaults to read-only; apps enable editing on writable connections.\n * The connection the browsing path uses remains read-only, so looking\n * around cannot write, even after editing is enabled. Editing goes through a\n * separate connection that the server opens lazily, the first time it is\n * actually asked to write.\n *\n * Editing preferences are remembered separately per connection and workspace.\n * Workbench enabling still requires explicit confirmation.\n * It never grants server permission or carries from one database to another.\n *\n * The real guard is on the server, not here. This file makes the state\n * obvious and asks for confirmation; the server independently refuses any\n * update whose WHERE clause does not match exactly one row.\n */\nimport { ownedBy } from './requestIdentity.js';\nimport {\n api,\n captureNavigation,\n columnLabel,\n currentEntry,\n el,\n loadJson,\n prefersPages,\n render,\n saveJson,\n setRecordMutationPendingProbe,\n state,\n toast,\n} from './core.js';\nimport { ask, confirmFor } from './prompt.js';\nimport { WriteConfirmDialog } from './components/WriteConfirm.js';\nimport { booleanChoices, canEditRow } from './rowEditing.js';\nimport { captureSessionIdentity, sessionIdentity, subscribeSessionIdentity } from './sessionIdentity.js';\nimport { updateAttempts, updateOwnerKey } from './updateAttempts.js';\nimport { createRecordDrafts, draftSchemaKey } from './recordDrafts.js';\nimport { shell } from './shellMode.js';\nimport { validateRecordEdits } from './recordEditValidation.js';\nimport { captureRecordWriteRefresh } from './recordWriteRefresh.js';\nimport { connectionGeneration } from './connectionTransition.js';\nimport { plural } from './locale.js';\nimport { say } from './copy/record/write.js';\nimport { isJsonType } from '@mistriwork/engine/shared/schema.js';\nimport { addHostListener, portalRoot, shellBody } from './shellHost.js';\n\nsetRecordMutationPendingProbe(recordAutosavePending);\n\n/* Write mode is remembered per connection.\n Per connection, not globally: switching from the demo to production must\n not silently inherit permission to edit it. Each database is opted into\n separately, and the choice sticks until it is turned off. */\nconst WRITE_KEY = 'mistri.write.v1';\nconst APP_WRITE_KEY = 'mistri.app-editing.v1';\n\nconst connectionKey = () => state.activeConnection ?? 'default';\nconst writableConnection = () => Boolean(state.connections?.find((c) => c.id === state.activeConnection)?.writable);\n\nfunction rememberedWrite() {\n if (prefersPages()) return writableConnection() && loadJson(APP_WRITE_KEY, {})[connectionKey()] !== false;\n return loadJson(WRITE_KEY, {})[connectionKey()] === true;\n}\n\nfunction rememberWrite(on) {\n const storageKey = prefersPages() ? APP_WRITE_KEY : WRITE_KEY;\n const all = loadJson(storageKey, {});\n all[connectionKey()] = on;\n saveJson(storageKey, all);\n}\n\nexport function initWriteMode() {\n // Restored without asking again: the confirmation was given for this\n // database and has not been withdrawn.\n setWriteMode(rememberedWrite(), { remember: false });\n}\n\n/** Mode changes must not hide pending work or interrupt an acknowledged save. */\nexport async function prepareEditingExit() {\n if (shell().mutations.hasUnresolved()) {\n toast(say('resolveThePendingOrUnconfirmed'), 'error');\n return false;\n }\n if (applyingStaged()) {\n toast(say('waitForThePending'), 'error');\n return false;\n }\n const connection = state.activeConnection;\n const schema = state.schema;\n const entry = currentEntry();\n const mode = prefersPages();\n const captured = staged;\n if (\n pendingCount() &&\n !(await confirmFor({\n title: say('discardPendingRecordChanges'),\n body: say('theseFieldChangesHaveNotBeen'),\n confirmLabel: say('discardChanges'),\n }))\n )\n return false;\n if (\n connection !== state.activeConnection ||\n schema !== state.schema ||\n entry !== currentEntry() ||\n mode !== prefersPages() ||\n captured !== staged ||\n applyingStaged() ||\n shell().mutations.hasUnresolved()\n )\n return false;\n discardStaged();\n return true;\n}\n\nexport async function toggleWriteMode() {\n if (state.canWrite) {\n if (await prepareEditingExit()) setWriteMode(false);\n return;\n }\n const connection = state.activeConnection;\n const schema = state.schema;\n const app = prefersPages();\n if (app && !writableConnection()) {\n toast(say('thisConnectionIsRead'), 'error');\n return;\n }\n if ((app || (await confirmWrite())) && connection === state.activeConnection && schema === state.schema && app === prefersPages())\n setWriteMode(true);\n}\n\n/** Re-evaluate on connection switch, since the setting is per database. */\nexport function syncWriteMode() {\n setWriteMode(rememberedWrite(), { remember: false });\n}\n\nfunction setWriteMode(on, { remember = true } = {}) {\n state.canWrite = on;\n if (remember) rememberWrite(on);\n /* The indicator is on <body> rather than the button, so it can colour the\n window frame. Being in write mode should be visible from across the room,\n not something you have to go looking for. */\n shellBody().classList.toggle('write-mode', on && !prefersPages());\n // Explicit mode exits discard above; automatic connection changes retain drafts.\n if (!on && remember) discardStaged();\n render();\n}\n\n/**\n * A real confirmation, naming the database — `components/WriteConfirm.tsx`.\n * \"Are you sure?\" teaches nothing; the name of the thing you are about to\n * be able to change does.\n */\nfunction confirmWrite() {\n return ask(WriteConfirmDialog, { label: state.schema.label });\n}\n\n/* ---------- staged edits ----------\n\n Every submission captures the whole draft as one update. An opted-in App\n can submit on an owned blur; Workbench keeps explicit review and apply.\n\n Staging is keyed by table and row, so walking away and back does not\n silently carry pending edits onto a different row. */\n\nconst drafts = createRecordDrafts();\nconst emptyDraft = () => ({\n connection: null,\n table: '',\n key: {},\n schemaKey: '',\n needsReview: false,\n manualSaveRequired: false,\n values: {},\n originals: {},\n inputs: {},\n});\nlet staged = emptyDraft();\nsubscribeSessionIdentity(() => {\n cancelRecordAutosave();\n drafts.clear();\n staged = emptyDraft();\n tell();\n});\n\nexport function activateDraft(table, key) {\n const next = drafts.acquire({ connection: state.activeConnection ?? null, table: table.id, key }, draftSchemaKey(table));\n if (next === staged) return staged;\n staged = next;\n tell();\n return staged;\n}\n\nexport function suspendDraft(draft) {\n const retained = draft === staged && prefersPages() && Object.keys(draft.values).length > 0 && !updateAttempts.get(draft);\n if (draft === staged) cancelRecordAutosave();\n drafts.suspend(draft);\n feedback.delete(draft);\n if (draft === staged) staged = emptyDraft();\n if (retained) toast(say('unsavedChangesWereRetained'), 'info');\n}\n\n/**\n * The record the reader has an unsaved draft of, for live sync (liveInvalidate.ts): a change another person made to\n * it does not reach the form, it is said beside it (components/LiveNotice.tsx).\n * @returns {{ table: string, key: Record<string, unknown>, originals: Record<string, unknown>, values: Record<string, unknown> } | undefined}\n */\nexport const dirtyDraft = () =>\n staged.connection === (state.activeConnection ?? null) && Object.keys(staged.values).length ? staged : undefined;\n\n/**\n * Take another person's value for one field of the draft: the form shows it, and Save checks against it.\n * @param {string} column\n * @param {unknown} value\n */\nexport function takeTheirs(column, value) {\n if (!Object.keys(staged.values).length) return;\n staged.values[column] = value;\n staged.originals[column] = value;\n tell();\n}\n\nexport const draftNeedsReview = (table, key) => sameRow(table, key) && staged.needsReview;\nexport const draftRequiresManualSave = (table, key) => sameRow(table, key) && staged.manualSaveRequired;\nexport const draftSchemaChanged = (table) => staged.schemaKey !== draftSchemaKey(table);\nexport const retainedValues = () => ({ ...staged.values });\nexport const stagedOriginal = (column) => (staged.needsReview ? undefined : staged.originals[column]);\n\nexport function resumeDraft(table, key) {\n if (!sameRow(table.id, key) || draftSchemaChanged(table) || !canEditRow(table, key) || stagedAttempt(table.id, key)) return;\n staged.needsReview = false;\n tell();\n}\n\naddHostListener(window, 'beforeunload', (event) => {\n if (!drafts.hasPending()) return;\n event.preventDefault();\n event.returnValue = '';\n});\n\nexport function pendingCount() {\n return staged.connection === (state.activeConnection ?? null) ? Object.keys(staged.values).length : 0;\n}\n\n/**\n * Whether what is staged belongs to the row on screen.\n *\n * The connection is part of the answer. Keyed by table and key alone, an edit\n * staged against staging's `customer:4` showed as pending on production's\n * `customer:4` — the same table name and the same id, a different database,\n * and one click from being written to the wrong one.\n */\nfunction sameRow(table, key) {\n return updateOwnerKey(staged) === updateOwnerKey({ connection: state.activeConnection ?? null, table, key });\n}\n\nexport function discardStaged() {\n cancelRecordAutosave();\n editErrors.delete(staged);\n clearServerErrors(staged);\n discards.set(staged, (discards.get(staged) ?? 0) + 1);\n if (staged.connection === (state.activeConnection ?? null)) {\n staged.values = {};\n staged.originals = {};\n staged.needsReview = false;\n staged.manualSaveRequired = false;\n feedback.delete(staged);\n } else staged = emptyDraft();\n tell();\n}\n\n/* A flag's two stored values and whether a row may be edited live in\n rowEditing.js, where a grid's cell asks them too; re-exported for the\n record editors that have always asked here. */\nexport { booleanChoices, canEditRow };\n\n/* The editor and the pending bar are components (components/Editable.tsx); they\n read this store and tick with it. */\nconst listeners = new Set();\nconst feedback = new WeakMap();\nconst discards = new WeakMap();\n/** @type {WeakMap<import('./recordDrafts.js').RecordDraft, Readonly<Record<string, string>>>} */\nconst editErrors = new WeakMap();\n/* The fields a refused save named, marked on the record until each is edited.\n Kept apart from the preflight's errors, which are recomputed from the draft\n on every edit and would otherwise wipe a mark the reader has not touched. */\n/** @type {WeakMap<import('./recordDrafts.js').RecordDraft, Readonly<Record<string, string>>>} */\nconst serverErrors = new WeakMap();\n/** @type {WeakMap<Readonly<Record<string, string>>, { client: Readonly<Record<string, string>>, errors: Readonly<Record<string, string>> }>} */\nconst mergedErrors = new WeakMap();\n/* The first field a refused explicit Save marked. Its editor may not be drawn\n yet — an unopened field opens because it is marked — so the editor takes\n the focus when it is, once. */\n/** @type {{ draft: import('./recordDrafts.js').RecordDraft, field: string } | null} */\nlet fieldFocus = null;\nconst noEditErrors = Object.freeze({});\nlet version = 0;\nfunction tell() {\n version += 1;\n for (const fn of listeners) fn();\n}\nexport function subscribeStaged(fn) {\n listeners.add(fn);\n const unsubscribe = updateAttempts.subscribe(fn);\n return () => {\n listeners.delete(fn);\n unsubscribe();\n };\n}\nexport const stagedVersion = () => version + updateAttempts.version();\n/** @returns {import('./updateAttempts.js').UpdateAttempt | undefined} */\nexport const stagedAttempt = (table, key) => updateAttempts.get({ connection: state.activeConnection ?? null, table, key });\nexport const stagedFeedback = (table, key) => (sameRow(table, key) ? feedback.get(staged) : undefined);\nexport const stagedDiscardVersion = (table, key) => (sameRow(table, key) ? (discards.get(staged) ?? 0) : 0);\n/** @returns {Readonly<Record<string, string>>} */\nexport const stagedEditErrors = (tableId, key) => (sameRow(tableId, key) ? draftErrors(staged) : noEditErrors);\n\n/* The preflight's word wins over a refusal's for the same field: it is about\n the value on screen now, the refusal about the one that was sent. */\nfunction draftErrors(draft) {\n const client = editErrors.get(draft) ?? noEditErrors;\n const server = serverErrors.get(draft);\n if (!server) return client;\n const cached = mergedErrors.get(server);\n if (cached?.client === client) return cached.errors;\n const errors = Object.freeze({ ...server, ...client });\n mergedErrors.set(server, { client, errors });\n return errors;\n}\n\n/** Whether this field's editor should take the focus a refused Save left for it. Consumed. */\nexport function takeFieldFocus(tableId, key, column) {\n if (!fieldFocus || fieldFocus.draft !== staged || fieldFocus.field !== column || !sameRow(tableId, key)) return false;\n fieldFocus = null;\n return true;\n}\n\n/* A column's own mark, and its row-defined fields' (`specs.capacity`): the\n whole object is what was staged again. */\nfunction clearServerError(draft, name) {\n const server = serverErrors.get(draft);\n const of = (field) => field === name || field.startsWith(`${name}.`);\n if (fieldFocus?.draft === draft && of(fieldFocus.field)) fieldFocus = null;\n if (!server || !Object.keys(server).some(of)) return;\n const rest = Object.fromEntries(Object.entries(server).filter(([field]) => !of(field)));\n if (Object.keys(rest).length) serverErrors.set(draft, Object.freeze(rest));\n else serverErrors.delete(draft);\n}\n\nfunction clearServerErrors(draft) {\n serverErrors.delete(draft);\n if (fieldFocus?.draft === draft) fieldFocus = null;\n}\n\n/* A field is on screen when its editor — open or not — is registered and\n drawn. Presence, not focusability: a pending save disables the launchers. */\nfunction fieldOnScreen(draft, name) {\n const root = draft.inputs[name]?.element;\n return Boolean(root?.isConnected && !root.closest('[hidden], [inert], [aria-hidden=\"true\"]'));\n}\n\n/**\n * Mark the fields a refused save named — the New form's contract, through the\n * same shape check — on the record they are drawn on, and say in the summary\n * only what cannot be marked: fields this record does not show, or none named\n * at all. An explicit Save leaves the focus on the first marked field in\n * catalog order; a blur save leaves it where the reader moved it.\n * @returns {string} The summary, empty when every named field is marked.\n */\nfunction markRefusal(draft, table, result, focus) {\n const marks = Object.create(null);\n const unplaced = [];\n for (const { field, message } of result.fields ?? []) {\n // A row-defined field is `column.key` (definedFields.ts), marked where its own editor is registered under that name.\n const column = table.columns?.find((candidate) => candidate.name === field.split('.')[0]);\n if (column && fieldOnScreen(draft, field)) marks[field] = message;\n else unplaced.push(column ? `${columnLabel(table.id, field) ?? field}: ${message}` : '');\n }\n const marked = Object.keys(marks);\n if (marked.length) serverErrors.set(draft, Object.freeze(marks));\n else clearServerErrors(draft);\n const first =\n table.columns?.find((column) => Object.hasOwn(marks, column.name))?.name ??\n marked.find((field) => table.columns?.some((column) => field.startsWith(`${column.name}.`)));\n fieldFocus = focus && first ? { draft, field: first } : fieldFocus?.draft === draft ? null : fieldFocus;\n if (marked.length && !unplaced.length) return '';\n return [result.message, ...unplaced.filter(Boolean)].join(' ');\n}\nexport function dismissSaveFeedback() {\n feedback.delete(staged);\n tell();\n}\n\nfunction readEditControls(draft) {\n /** @type {Record<string, import('./recordEditValidation.js').RecordEditControlState>} */\n const controls = Object.create(null);\n for (const [name, registered] of Object.entries(draft.inputs)) {\n if (!registered.element.isConnected || !registered.readState) continue;\n try {\n const value = registered.readState();\n if (!value || typeof value !== 'object' || Array.isArray(value) || 'then' in value) throw new Error();\n const { unresolvedReference, invalidInput } = value;\n if (\n (unresolvedReference !== undefined && typeof unresolvedReference !== 'boolean') ||\n (invalidInput !== undefined && typeof invalidInput !== 'boolean')\n )\n throw new Error();\n controls[name] = { unresolvedReference, invalidInput };\n } catch {\n // Control faults never expose callback diagnostics or permit a partial save.\n controls[name] = { invalidInput: true };\n }\n }\n return controls;\n}\n\nfunction errorControl(draft, name) {\n const root = draft.inputs[name]?.element;\n if (!root?.isConnected || root.closest('[hidden], [inert], [aria-hidden=\"true\"]')) return;\n const selector =\n 'input:not(:disabled):not([type=\"hidden\"]), textarea:not(:disabled), select:not(:disabled), button:not(:disabled), [tabindex]:not([tabindex=\"-1\"])';\n return root.matches(selector) ? root : root.querySelector(selector);\n}\n\nfunction publishEditValidation(draft, errors) {\n const previous = editErrors.get(draft) ?? noEditErrors;\n const names = Object.keys(errors);\n let changed = names.length !== Object.keys(previous).length || names.some((name) => errors[name] !== previous[name]);\n editErrors.set(draft, changed ? errors : previous);\n const inaccessible = names.filter((name) => !errorControl(draft, name));\n const priorFeedback = feedback.get(draft);\n if (inaccessible.length) {\n const message = inaccessible.map((name) => `${name}: ${errors[name]}`).join(' ');\n if (priorFeedback?.source !== 'validation' || priorFeedback.message !== message) {\n feedback.set(draft, { phase: 'error', source: 'validation', message });\n changed = true;\n }\n } else if (priorFeedback?.source === 'validation') {\n feedback.delete(draft);\n changed = true;\n }\n return changed;\n}\n\n/** Corrections may clear attempted validation, but mounting stays quiet. */\nexport function refreshEditValidation(table, key) {\n if (!prefersPages() || !sameRow(table.id, key) || !editErrors.has(staged)) return;\n const captured = staged,\n revision = stagedVersion();\n const currentTable = state.schema?.tables?.find((candidate) => candidate.id === table.id) ?? table;\n const errors = validateRecordEdits(currentTable, captured.values, readEditControls(captured));\n if (captured !== staged || revision !== stagedVersion() || !sameRow(table.id, key)) return;\n if (publishEditValidation(captured, errors)) tell();\n}\n\n/** What is staged for a column of the row on screen, or undefined. */\nexport const stagedValue = (column) => (staged.needsReview ? undefined : staged.values[column]);\nexport const pendingColumns = () => Object.keys(staged.values);\n\n/**\n * Stage one change. Typing a value and then typing the original back is not\n * a change. Exported for the editor component.\n */\nexport function stage(table, key, column, value, original) {\n const known = table.columns?.find((c) => c.name === column.name);\n // A declared transition moves only through the record's actions.\n if (column.writable === false || column.changesBy === 'actions' || known?.writable === false || known?.changesBy === 'actions') return;\n // Freeze before an autosave publishes pending state. Other staged operations\n // (notably Undo reconciliation) still preserve edits begun while they settle.\n if (stagedAttempt(table.id, key) || (recordAutosavePending() && sameRow(table.id, key))) return;\n activateDraft(table, key);\n if (staged.needsReview) return;\n feedback.delete(staged);\n // An edited field is the reader's answer to its mark; the others keep theirs.\n clearServerError(staged, column.name);\n if (Object.hasOwn(staged.originals, column.name)) original = staged.originals[column.name];\n // Text inputs stringify numbers and booleans, but NULL is not the text \"null\".\n const unchanged = value === null || original === null ? value === original : String(value) === String(original);\n if (unchanged) {\n delete staged.values[column.name];\n delete staged.originals[column.name];\n } else {\n staged.values[column.name] = value;\n staged.originals[column.name] = original;\n /* A row-defined field (DefinedFields.tsx) is staged under `column.key`\n and saved as its column's whole object: the object it was read in and\n how its box's text becomes a value ride with the draft. */\n if (column.defined) (staged.folds ??= {})[column.name] = column.defined;\n }\n refreshEditValidation(table, key);\n tell();\n}\n\n/* What is sent: each row-defined field folded into its column's object, as\n JSON text (the server writes and judges the whole object). */\nfunction folded(draft, values, originals) {\n const folds = draft.folds ?? {};\n const fields = Object.keys(values).filter((name) => Object.hasOwn(folds, name));\n if (!fields.length) return { values, originals };\n const objects = {};\n const sent = { ...values },\n guarded = { ...originals };\n for (const name of fields) {\n const { column, object, toValue } = folds[name];\n const next = (objects[column] ??= { ...object });\n const value = toValue(values[name]);\n if (value === undefined) delete next[name.slice(column.length + 1)];\n else next[name.slice(column.length + 1)] = value;\n delete sent[name];\n delete guarded[name];\n }\n for (const [column, object] of Object.entries(objects)) sent[column] = JSON.stringify(object);\n return { values: sent, originals: guarded };\n}\n\n/** The editor's element, kept so a successful write can confirm in place.\n * @param {() => import('./recordEditValidation.js').RecordEditControlState} [readState]\n */\nexport function registerInput(table, key, column, input, onCommitted, readState) {\n activateDraft(table, key);\n const captured = staged,\n registration = { element: input, onCommitted, readState };\n Object.defineProperty(captured.inputs, column, { value: registration, configurable: true, writable: true, enumerable: true });\n return () => {\n if (captured.inputs[column] === registration) delete captured.inputs[column];\n };\n}\n\nexport { sameRow };\n\n/* An update's guard names each changed column at the value it was read with —\n but a JSON column's, which no engine compares: it is written whole. */\nfunction comparable(table, originals) {\n const json = new Set((table.columns ?? []).filter((column) => isJsonType(column.type)).map((column) => column.name));\n return json.size ? Object.fromEntries(Object.entries(originals).filter(([column]) => !json.has(column))) : originals;\n}\n\nlet applying = false;\nexport const applyingStaged = () => applying;\nlet autosaveCancellation = 0;\n/** @type {ReturnType<typeof captureSessionIdentity> | undefined} */\nlet autosaveIdentity;\n/** @type {import('./updateAttempts.js').UpdateOwner | undefined} */\nlet autosaveOwner;\n/** Deferred intent only: an already dispatched write still settles normally. */\nexport function cancelRecordAutosave() {\n autosaveCancellation++;\n}\nexport function recordAutosavePending() {\n // Ordinary uncertainty keeps the record locked, not every route. Tenant\n // uncertainty still has its independent stronger navigation gate in core.\n return (\n autosaveIdentity !== undefined &&\n autosaveIdentity === sessionIdentity() &&\n autosaveOwner !== undefined &&\n updateAttempts.get(autosaveOwner)?.phase !== 'uncertain'\n );\n}\n\n/** Capture only for a fresh user edit leaving its complete interaction surface.\n * This predicate does not schedule or send anything. The interaction owner must\n * also prove its mount, picker state and current App save policy at dispatch. */\nexport function captureRecordAutosaveGuard(table, key, stillEditing) {\n const captured = staged,\n identity = captureSessionIdentity(),\n schema = state.schema;\n const capturedKey = { ...key },\n revision = stagedVersion();\n const cancellation = autosaveCancellation;\n const navigation = captureNavigation();\n const current = () => {\n try {\n return (\n typeof stillEditing === 'function' &&\n stillEditing() === true &&\n cancellation === autosaveCancellation &&\n prefersPages() &&\n navigation() &&\n captured === staged &&\n identity === sessionIdentity() &&\n schema === state.schema &&\n revision === stagedVersion() &&\n sameRow(table.id, capturedKey) &&\n pendingCount() > 0 &&\n !staged.needsReview &&\n !staged.manualSaveRequired &&\n !applying &&\n !stagedAttempt(table.id, capturedKey) &&\n !draftSchemaChanged(table) &&\n canEditRow(table, capturedKey)\n );\n } catch {\n return false;\n }\n };\n // An initially ineligible draft cannot be armed later by Resume, policy\n // changes, another acknowledgement or programmatic focus restoration.\n return current() ? current : () => false;\n}\n\n/**\n * @param {(opts?: { inPlace?: boolean }) => void} [onDone]\n * @param {{ trigger?: 'explicit'|'blur', guard?: () => boolean }} [options]\n */\nexport async function applyStaged(table, key, onDone = undefined, options = {}) {\n const trigger = options.trigger ?? 'explicit';\n if (trigger !== 'explicit' && trigger !== 'blur') return;\n const blur = trigger === 'blur';\n // Do not cancel the supplied blur capture before checking it.\n if (!blur) cancelRecordAutosave();\n const guard = () => {\n try {\n return typeof options.guard === 'function' && options.guard() === true;\n } catch {\n return false;\n }\n };\n if (blur && (!prefersPages() || staged.manualSaveRequired || !guard())) return;\n if (applying || staged.needsReview || stagedAttempt(table.id, key) || !sameRow(table.id, key)) return;\n if (!pendingCount()) {\n if (!blur && staged.manualSaveRequired) {\n staged.manualSaveRequired = false;\n editErrors.delete(staged);\n clearServerErrors(staged);\n feedback.delete(staged);\n tell();\n }\n return;\n }\n const currentTable = state.schema?.tables ? state.schema.tables.find((candidate) => candidate.id === table.id) : table;\n if (\n !currentTable ||\n draftSchemaChanged(currentTable) ||\n draftSchemaChanged(table) ||\n Object.keys(staged.values).some((name) => {\n const column = currentTable.columns?.find((c) => c.name === name);\n return column?.writable === false || column?.changesBy === 'actions';\n })\n )\n return;\n const app = prefersPages();\n if (app && !canEditRow(currentTable, key)) return;\n const captured = staged;\n const connection = captured.connection;\n const staging = { ...staged.values };\n const { values, originals } = folded(captured, staging, { ...staged.originals });\n key = { ...key };\n const schema = state.schema;\n const identity = captureSessionIdentity();\n const generation = connectionGeneration();\n const navigation = blur ? captureNavigation() : () => true;\n const cancellation = autosaveCancellation,\n revision = stagedVersion();\n if (app) {\n const errors = validateRecordEdits(currentTable, values, readEditControls(captured));\n if (\n captured !== staged ||\n revision !== stagedVersion() ||\n schema !== state.schema ||\n identity !== sessionIdentity() ||\n cancellation !== autosaveCancellation ||\n !sameRow(table.id, key) ||\n !canEditRow(currentTable, key) ||\n (blur && !guard())\n )\n return;\n const changed = publishEditValidation(captured, errors);\n if (Object.keys(errors).length) {\n if (changed) tell();\n if (!blur && captured === staged && identity === sessionIdentity() && sameRow(table.id, key)) {\n const first = Object.keys(errors)\n .map((name) => errorControl(captured, name))\n .find(Boolean);\n first?.focus();\n }\n return;\n }\n }\n feedback.delete(captured);\n const current = () => identity === sessionIdentity() && captured === staged && sameRow(table.id, key) && state.canWrite;\n const dispatchCurrent = () =>\n current() &&\n schema === state.schema &&\n generation === connectionGeneration() &&\n !draftSchemaChanged(table) &&\n (!app || (prefersPages() && canEditRow(currentTable, key))) &&\n (!blur || (navigation() && cancellation === autosaveCancellation && !captured.manualSaveRequired && !captured.needsReview));\n applying = true;\n if (blur) {\n autosaveIdentity = identity;\n autosaveOwner = { connection: connection ?? null, table: table.id, key: { ...key } };\n }\n try {\n tell();\n const ok = app || (await confirmUpdate(table, key, values, originals));\n if (!ok) return;\n if (!dispatchCurrent()) {\n if (!blur) toast(say('theRecordConnectionOrTable'), 'error');\n return;\n }\n /** @type {(() => void) | undefined} */\n let acknowledged;\n const result = await updateAttempts.run({ connection: connection ?? null, table: table.id, key }, values, () => {\n if (!dispatchCurrent()) throw Object.assign(new Error(say('theRecordChangedBefore')), { outcome: 'not_applied' });\n if (app) acknowledged = captureRecordWriteRefresh(table.id, key);\n return api('/api/update', { ...ownedBy(connection), table: table.id, key, values, expected: comparable(table, originals) });\n });\n if (result.kind !== 'applied') {\n if (result.kind === 'rejected') captured.manualSaveRequired = true;\n if (app && current() && result.kind === 'rejected') {\n /* The draft stays as it was; the fields the server named are marked\n on it, and nothing is retried — a blur save included. */\n const summary = markRefusal(captured, currentTable, result, !blur);\n if (summary) feedback.set(captured, { phase: 'error', message: summary });\n else feedback.delete(captured);\n tell();\n } else if (identity === sessionIdentity() && (result.kind !== 'uncertain' || !current())) toast(result.message, 'error');\n return;\n }\n const n = Object.keys(result.applied).length;\n const inputs = captured.inputs;\n // Settle the original draft even after navigation selected another record.\n for (const [column, value] of Object.entries(result.applied)) {\n if (Object.is(captured.values[column], values[column])) {\n delete captured.values[column];\n delete captured.originals[column];\n } else if (column in captured.values) captured.originals[column] = value;\n }\n // A row-defined field folded into a column that was written, unless it was edited again meanwhile.\n const foldedSaved = {};\n for (const name of Object.keys(staging)) {\n const fold = captured.folds?.[name];\n if (!fold || !Object.hasOwn(result.applied, fold.column)) continue;\n // Its editor is told what it now holds: the box's own text, which the object was written from.\n foldedSaved[name] = staging[name];\n if (!Object.is(captured.values[name], staging[name])) continue;\n delete captured.values[name];\n delete captured.originals[name];\n delete captured.folds[name];\n }\n if (!Object.keys(captured.values).length) {\n captured.needsReview = false;\n captured.manualSaveRequired = false;\n editErrors.delete(captured);\n }\n // What a refusal said was about the values it saw; these were written.\n clearServerErrors(captured);\n acknowledged?.();\n // History follows the original owner even if another record is now open.\n if (\n identity === sessionIdentity() &&\n connection === state.activeConnection &&\n schema === state.schema &&\n generation === connectionGeneration()\n )\n void shell().undo.refresh();\n tell();\n if (!current()) {\n if (identity === sessionIdentity())\n toast(say('updatedColumnsOnTheOriginal', { count: n, connection: connection ?? say('theOriginalConnection') }), 'ok');\n return;\n }\n\n // An App has no Undo: the dock says what was saved.\n if (app) feedback.set(captured, { phase: 'saved', message: say('changesSaved'), identity, columns: Object.keys(values) });\n else\n toast(\n say('updatedColumns', { count: n, name: table.name }),\n 'ok',\n !identity.tenantRequired && result.undoId\n ? {\n /* Beside the confirmation, because this is the moment someone realises\n it was the wrong row — and sending them to a menu to find Undo wastes\n the seconds in which they still remember what they changed. */\n label: say('common.undo'),\n run: () => {\n if (identity === sessionIdentity() && connection === state.activeConnection) void shell().undo.last(result.undoId);\n },\n }\n : undefined,\n );\n\n /* Confirmed in place rather than by re-rendering. A re-render would throw\n away the row you are looking at and rebuild it, which reads as a flicker\n and loses the caret; worse, it left the fields showing their unsaved\n styling because the DOM they styled was gone before the class was.\n Showing the saved value, in green, on the field you just edited is both\n calmer and more specific — it says *which* fields landed. */\n tell();\n confirmSaved(inputs, { ...result.applied, ...foldedSaved });\n // Only the pending bar is refreshed, so the green fields survive.\n onDone?.({ inPlace: true });\n } catch (err) {\n // Attempt failures are classified above; presentation failures must not resend.\n toast(err.message, 'error');\n } finally {\n if (blur) {\n autosaveIdentity = undefined;\n autosaveOwner = undefined;\n }\n applying = false;\n tell();\n }\n}\n\nfunction confirmSaved(inputs, applied) {\n for (const [column, value] of Object.entries(applied)) {\n const registered = inputs[column];\n const input = registered?.element;\n if (!input || !input.isConnected) continue;\n // The value the database actually stored, which may differ from what was\n // typed — \"17901\" comes back as the number 17901.\n registered.onCommitted?.(value);\n }\n}\n\n/** Show the statement before it runs. This is the last stop before a write. */\nfunction confirmUpdate(table, key, values, originals) {\n return new Promise((resolve) => {\n const q = (id) => `\"${id}\"`;\n const sets = Object.keys(values)\n .map((c) => `${q(c)} = ?`)\n .join(',\\n ');\n const where = [\n ...Object.keys(key).map((c) => `${q(c)} = ?`),\n ...Object.keys(originals).map((c) => `${q(c)} ${originals[c] === null ? 'IS NULL' : '= ?'}`),\n ].join(' AND ');\n const sql = `UPDATE ${q(table.id)}\\n SET ${sets}\\n WHERE ${where};`;\n\n /* The parameters shown are the raw staged values. The server coerces them\n against the declared column types and returns what it applied, so a\n number typed as text is corrected there rather than guessed at here. */\n const params = JSON.stringify([\n ...Object.values(values),\n ...Object.values(key),\n ...Object.values(originals).filter((value) => value !== null),\n ]);\n\n const rows = Object.entries(values).map(([column, to]) => {\n const from = originals[column];\n const type = table.columns.find((c) => c.name === column)?.type ?? '';\n return el('tr', {}, [\n el('td', {}, [el('span', { class: 'diff-col', text: column }), el('span', { class: 'diff-type', text: type.toLowerCase() })]),\n el('td', { class: `diff-from${from === null ? ' null' : ''}`, text: from === null ? 'null' : String(from) }),\n el('td', { class: 'diff-arrow', text: '→' }),\n el('td', { class: `diff-to${to === null ? ' null' : ''}`, text: to === null ? 'null' : String(to) }),\n ]);\n });\n\n const dialog = el('dialog', { class: 'confirm confirm-wide' }, [\n el('h2', { text: rows.length === 1 ? say('applyThisChange') : say('applyChanges', { count: rows.length }) }),\n el('p', {}, [\n document.createTextNode('One row in '),\n el('strong', { text: table.id }),\n document.createTextNode(', identified by '),\n el('strong', {\n text: Object.entries(key)\n .map(([k, v]) => `${k} = ${v}`)\n .join(', '),\n }),\n document.createTextNode('. Applied as a single statement only if these fields still match their original values.'),\n ]),\n el('table', { class: 'confirm-diff' }, el('tbody', {}, rows)),\n el('pre', { class: 'ddl', text: sql }),\n el('p', { class: 'confirm-params', text: say('parameters', { params }) }),\n el('div', { class: 'confirm-actions' }, [\n el('button', {\n type: 'button',\n class: 'ghost',\n text: say('common.cancel'),\n onclick: () => {\n dialog.close();\n resolve(false);\n },\n }),\n el('button', {\n type: 'button',\n class: 'primary',\n text: say('common.apply'),\n onclick: () => {\n dialog.close();\n resolve(true);\n },\n }),\n ]),\n ]);\n dialog.addEventListener('close', () => dialog.remove());\n dialog.addEventListener('cancel', () => resolve(false));\n portalRoot().append(dialog);\n dialog.showModal();\n });\n}\n\n/** Why a row is not editable, for the record view to explain rather than hide. */\nexport function whyNotEditable(table) {\n if (!state.canWrite) return null; // not in write mode: say nothing\n if (!table) return say('unknownTable');\n if (table.isView) return say('thisIsAView');\n if (!table.columns.some((c) => c.primaryKey)) {\n return say('thisTableHasNoPrimary');\n }\n return null;\n}\n"],"mappings":"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"}