@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 +1 @@
1
- {"version":3,"file":"shared-DPAVoWY0.js","names":["say","say","say","say","findTable","sharedFindTable","sharedLabelPath"],"sources":["../../../engine/src/shared/pageRoutes.ts","../../../kit/src/client/connectionTransition.ts","../../../kit/src/client/copy/shell/toast.ts","../../../kit/src/client/toast.ts","../../../kit/src/client/copy/shell/requestIdentity.ts","../../../kit/src/client/requestIdentity.ts","../../../kit/src/client/historyToken.ts","../../../kit/src/client/composedHook.ts","../../../kit/src/client/ownWrites.ts","../../../kit/src/client/recordTrail.ts","../../../kit/src/client/dom.js","../../../engine/src/shared/rollups.ts","../../../engine/src/shared/viewPaths.ts","../../../engine/src/shared/rowScope.ts","../../../engine/src/shared/view.ts","../../../kit/src/client/copy/shell/core.ts","../../../kit/src/client/core.js"],"sourcesContent":["/**\n * A free-form page's two words on the wire, said by the client that sends\n * them and the server that reads them (server/http/pageLoad.ts). Apart from\n * the SDK's module lists so the shell's first chunk carries only these.\n */\n\n/** Where a page's loader is run for the reader: `POST { page, params }`. */\nexport const PAGE_LOAD_PATH = '/api/pages/load';\n/** The header a page's own requests carry: its path. */\nexport const PAGE_HEADER = 'x-mistri-page';\n","/** One owner for picker switches, deep-link switches and schema refreshes. */\n/** Tiny lifetime signal in the existing eager owner. Flag state/validation are\n * imported only by the SDK; an App without components pays no transport cost. */\nlet presentationGeneration = 0,\n presentationAvailable = false;\nconst presentationListeners = new Set<() => void>();\nexport const appPresentation = () => ({ generation: presentationGeneration, available: presentationAvailable });\nexport function replaceAppPresentation(ready = false): void {\n ++presentationGeneration;\n presentationAvailable = ready;\n for (const listener of [...presentationListeners]) {\n try {\n listener();\n } catch {\n /* Observers cannot veto retirement. */\n }\n }\n}\nexport function subscribeAppPresentation(listener: () => void): () => void {\n presentationListeners.add(listener);\n return () => {\n presentationListeners.delete(listener);\n };\n}\nlet generation = 0;\nlet pending = false;\n\nexport const connectionGeneration = (): number => generation;\nexport const connectionTransitionPending = (): boolean => pending;\n\nexport function beginConnectionTransition() {\n replaceAppPresentation();\n const started = ++generation;\n pending = true;\n const isCurrent = () => started === generation;\n return {\n isCurrent,\n finish: () => {\n if (isCurrent()) pending = false;\n },\n };\n}\n","/** What toast says, in the shell room (packages/kit/src/client/messages.ts says how the catalog is split). */\nimport { defineMessages } from '../../messages.js';\n\nexport const say = defineMessages({\n dismissNotification: 'Dismiss notification',\n});\n","import { say } from './copy/shell/toast.js';\n\ninterface ToastAction {\n label: string;\n run: () => unknown;\n}\n\nconst active = new WeakMap<HTMLElement, () => void>();\nconst spoken = new WeakMap<HTMLElement, ReturnType<typeof setTimeout>>();\n\n/* The box is shown and hidden, so it cannot be the live region: a status\n that enters the accessibility tree already holding its message is one a\n screen reader never hears change, and \"Saved\" or \"Moved to Done\" went\n unsaid. The words go to a region that is always there (the box's\n `data-live`), emptied first and filled a beat later, so the same message\n twice is said twice. An error goes to the assertive region (`data-alert`)\n instead: a refused save is news that should interrupt, and a polite\n status waited behind whatever was being read. */\nfunction announce(box: HTMLElement, message: string, urgent: boolean) {\n const id = urgent ? (box.dataset.alert ?? box.dataset.live) : box.dataset.live;\n const live = id ? box.ownerDocument.getElementById(id) : null;\n if (!live) return;\n clearTimeout(spoken.get(live));\n live.textContent = '';\n spoken.set(\n live,\n setTimeout(() => {\n live.textContent = message;\n }, 50),\n );\n}\n\n/** One notification per host, with its timer owned by that notification. */\nexport function showToast(box: HTMLElement, message: string, tone = 'info', action?: ToastAction, app = false) {\n active.get(box)?.();\n const previousFocus = box.ownerDocument.activeElement;\n /* An error does not time out (WCAG 2.2.1): it says what went wrong and\n often what to do, and four seconds was less than it took to read. It\n stays until it is dismissed or replaced. */\n const persists = tone === 'error';\n let remaining = action ? 9000 : 4000;\n let started = 0;\n let timer: ReturnType<typeof setTimeout> | undefined;\n let hovered = false;\n let dismissed = false;\n const focusOut = () => queueMicrotask(resume);\n const pause = () => {\n if (timer === undefined) return;\n clearTimeout(timer);\n timer = undefined;\n remaining = Math.max(0, remaining - (Date.now() - started));\n };\n const dispose = () => {\n dismissed = true;\n pause();\n box.onmouseenter = box.onmouseleave = null;\n box.removeEventListener('focusin', pause);\n box.removeEventListener('focusout', focusOut);\n };\n const dismiss = () => {\n const hadFocus = box.contains(box.ownerDocument.activeElement);\n dispose();\n box.hidden = true;\n if (hadFocus && previousFocus instanceof HTMLElement && previousFocus.isConnected && !box.contains(previousFocus)) {\n previousFocus.focus({ preventScroll: true });\n }\n };\n const resume = () => {\n if (persists || dismissed || hovered || box.contains(box.ownerDocument.activeElement) || timer !== undefined) return;\n started = Date.now();\n timer = setTimeout(dismiss, remaining);\n };\n const span = (className: string, text: string) => {\n const node = box.ownerDocument.createElement('span');\n node.className = className;\n node.textContent = text;\n return node;\n };\n box.className = `toast toast-${tone}`;\n box.replaceChildren();\n if (app) {\n const icon = span('toast-icon', tone === 'ok' ? '\\u2713' : tone === 'error' ? '!' : 'i');\n icon.setAttribute('aria-hidden', 'true');\n box.append(icon);\n }\n box.append(span('toast-message', message));\n announce(box, message, tone === 'error');\n if (action) {\n const button = box.ownerDocument.createElement('button');\n button.type = 'button';\n button.className = 'toast-action';\n button.textContent = action.label;\n button.onclick = () => {\n if (dismissed) return;\n dismiss();\n action.run();\n };\n box.append(button);\n }\n // An error that stays needs a way to go, in the Workbench as in an App.\n if (app || persists) {\n const close = box.ownerDocument.createElement('button');\n close.type = 'button';\n close.className = 'toast-dismiss';\n close.textContent = '\\u00d7';\n close.setAttribute('aria-label', say('dismissNotification'));\n close.title = say('dismissNotification');\n close.onclick = dismiss;\n box.append(close);\n }\n box.onmouseenter = () => {\n hovered = true;\n pause();\n };\n box.onmouseleave = () => {\n hovered = false;\n resume();\n };\n box.addEventListener('focusin', pause);\n box.addEventListener('focusout', focusOut);\n active.set(box, dispose);\n box.hidden = false;\n resume();\n}\n","/** What requestIdentity says, in the shell room (packages/kit/src/client/messages.ts says how the catalog is split). */\nimport { defineMessages } from '../../messages.js';\n\nexport const say = defineMessages({\n noApp: 'No App at /{requested}.',\n invalidAuthenticationRequestIdentity: 'Invalid authentication request identity.',\n databaseConnectionManagementIsUnavailable: 'Database connection management is unavailable to applications.',\n applicationRequestsIdentifyAnApp: 'Application requests identify an app, not a connection.',\n invalidRequestIdentity: 'Invalid {field} request identity.',\n twoDifferentApplications: 'This request names two different applications.',\n twoDifferentConnections: 'This request names two different connections.',\n});\n","import { shell } from './shellMode.js';\nimport { say } from './copy/shell/requestIdentity.js';\n\nlet appConfigured: boolean | null = false;\n\n/** An invalid server contract is a startup failure, not an offline database. */\nexport class RuntimeMetadataError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'RuntimeMetadataError';\n }\n}\n\n/**\n * The address names no App this server serves: `/helpdesk` on a server whose\n * one App is at `/help-desk`. A reader's typo or a stale link, not an outage —\n * the screen says which Apps are here rather than blaming the database.\n */\nexport class UnknownAppError extends Error {\n constructor(\n readonly requested: string,\n readonly apps: ReadonlyArray<{ id: string; name: string }>,\n ) {\n super(say('noApp', { requested }));\n this.name = 'UnknownAppError';\n }\n}\n\n/** Listener identity, independent of the App/Workbench presentation toggle. */\nexport function setAppRequestScope(configured: boolean | null): void {\n appConfigured = configured;\n}\n\nexport const appRequestScope = (): boolean | null => appConfigured;\n\n/* A tenant App's visitor's public address (addressMount.js keepPlacement): every\n request carries it, for it alone places the visitor's reads with a tenant. */\nlet placement: string | null = null;\nexport function setPublicPlacement(via: string | null): void {\n placement = via;\n}\n\n/** The one selector this listener's server reads: `app` on an App server, `conn` in the explorer. */\nconst ownerField = (): 'app' | 'conn' => (appConfigured ? 'app' : 'conn');\n\n/**\n * A request's owner, under the selector the listener reads. Callers capture\n * the owner when their work starts — an App ID on an App listener, a registry\n * ID in the explorer — so an async draft, preview or cached read stays with\n * the source it began on. An absent owner leaves the active one to the\n * transport.\n */\nexport function ownedBy(owner: string | null | undefined): Record<string, string> {\n return owner == null ? {} : { [ownerField()]: owner };\n}\n\nexport function scopeApiRequest(\n path: string,\n body: Record<string, unknown> | undefined,\n active: string | null | undefined,\n): { url: string; payload: Record<string, unknown> | undefined } {\n const url = new URL(path, 'http://mistri.invalid');\n if (url.pathname === '/api/whoami') {\n if (\n url.searchParams.getAll('app').length > 1 ||\n url.searchParams.getAll('via').length > 1 ||\n [...url.searchParams.keys()].some((key) => key !== 'app' && key !== 'via')\n )\n throw new Error(say('invalidAuthenticationRequestIdentity'));\n return { url: `${url.pathname}${url.search}`, payload: body };\n }\n if (url.pathname === '/api/runtime')\n return { url: placement ? `${url.pathname}?via=${encodeURIComponent(placement)}` : url.pathname, payload: body };\n if (/^\\/api\\/connections(?:\\/|$)/.test(url.pathname)) {\n if (appConfigured !== false) throw new Error(say('databaseConnectionManagementIsUnavailable'));\n return { url: path, payload: body };\n }\n if (appConfigured === null) throw new Error(say('common.applicationRuntimeIsUnavailable'));\n\n const field = ownerField();\n // As the server does: an App request names an app, never a connection.\n if (appConfigured && (url.searchParams.has('conn') || body?.conn !== undefined)) throw new Error(say('applicationRequestsIdentifyAnApp'));\n const owners = [...url.searchParams.getAll(field), ...(body?.[field] == null ? [] : [body[field]])];\n if (owners.some((owner) => typeof owner !== 'string' || !owner.trim())) throw new Error(say('invalidRequestIdentity', { field }));\n const distinct = [...new Set(owners as string[])];\n if (distinct.length > 1) throw new Error(say(appConfigured ? 'twoDifferentApplications' : 'twoDifferentConnections'));\n const owner = distinct[0] ?? active;\n let payload = body;\n if (owner) {\n // Commands fingerprint a strict envelope; routing identity belongs to the URL.\n if (appConfigured && url.pathname.startsWith('/api/commands/')) {\n url.searchParams.set('app', owner);\n if (payload) {\n const { app: _app, ...envelope } = payload;\n payload = envelope;\n }\n } else if (payload) payload = { ...payload, [field]: owner };\n else url.searchParams.set(field, owner);\n }\n // Only on a visitor's read, as the shell names them: every other API route refuses an address (server tenantAuthenticatedHttp.ts).\n if (placement && shell().publicRead(url.pathname)) url.searchParams.set('via', placement);\n return { url: `${url.pathname}${url.search}${url.hash}`, payload };\n}\n","export type HistoryToken = number | string;\n\n/** Match tenantRecordHistory's tenantIdentifier(value, 128), not UUID syntax.\n * Validation recognizes a token's shape; only the server can authorize it. */\nexport function historyToken(value: unknown, tenant: boolean): HistoryToken | undefined {\n if (!tenant) return typeof value === 'number' && Number.isSafeInteger(value) && value > 0 ? value : undefined;\n return typeof value === 'string' && value.length > 0 && value.length <= 128 && value === value.trim() && !/\\p{Cc}/u.test(value)\n ? value\n : undefined;\n}\n\n/** One caller-owned connection/context. Never reuse it across owner changes. */\nexport type HistoryQuarantine = Readonly<{ tenant: false; ceiling: number } | { tenant: true; blocked: boolean }>;\n\nexport function createHistoryQuarantine(tenant: boolean): HistoryQuarantine {\n return tenant ? Object.freeze({ tenant: true, blocked: false }) : Object.freeze({ tenant: false, ceiling: 0 });\n}\n\n/** No lexical or numeric ordering exists for opaque tenant tokens. A lost Undo\n * therefore blocks the entire context, including subsequently previewed tokens.\n * An invalid ordinary token cannot establish a safe ceiling: fail closed. */\nexport function quarantineHistory(state: HistoryQuarantine, value: unknown): HistoryQuarantine {\n if (state.tenant === true) return Object.freeze({ tenant: true, blocked: true });\n const token = historyToken(value, false);\n return Object.freeze({ tenant: false, ceiling: Math.max(state.ceiling, typeof token === 'number' ? token : Number.MAX_SAFE_INTEGER) });\n}\n\nexport function historyTokenAvailable(state: HistoryQuarantine, value: unknown): boolean {\n const token = historyToken(value, state.tenant);\n if (token === undefined) return false;\n return state.tenant === true ? !state.blocked : typeof token === 'number' && token > state.ceiling;\n}\n","/**\n * Where the request path (core.js) meets a record page's composed answers\n * (composedReads.ts) without importing them: they tell it of themselves as\n * they load, so a page that opens no record page — the public page — carries\n * none of them. Depends on nothing, so either side may load first.\n */\ninterface ComposedReads {\n /** The answer a compose brought for this request, once. */\n reply(path: string, body: Record<string, unknown>): Promise<unknown>;\n /** Whether a compose kept, or on its way, could answer this path. */\n may(path: string): boolean;\n /** Every answer forgotten: a write makes them the past. */\n forget(): void;\n}\n\nlet reads: ComposedReads | null = null;\n\nexport function setComposedReads(given: ComposedReads): void {\n reads = given;\n}\n\n/** The composed answers, where a module that reads record pages has loaded them. */\nexport const composedReads = (): ComposedReads | null => reads;\n\n/* The connection a request that names none goes to: core.js says, as it loads. */\nlet connection: () => string | null = () => null;\n\nexport function setComposedConnection(source: () => string | null): void {\n connection = source;\n}\n\nexport const composedConnection = (): string | null => connection();\n","/**\n * This page's own writes, for the live stream that echoes them back.\n *\n * Every write the page sends is answered, and its caller then asks again what\n * it changed (recordWriteRefresh.ts). The same write then comes back on the\n * live stream, marked `self` (the reader's own account), a few hundred\n * milliseconds later. Taken as a change someone else made, it asked every read\n * the write's own refresh had just begun once more: a read that began before\n * the echo arrived had, by live sync's rule, begun before the change, so the\n * record page composed twice and every list and count on it was read twice,\n * and the composed answers were forgotten as a write's are, so each section\n * then asked on its own (a field report: 65 to 112 requests after one write).\n *\n * A change is in a read that began after it committed. The page's own write\n * had committed by the time it was answered, so its echo is measured from that\n * moment instead (liveInvalidate.ts): a read begun since already shows it and\n * is left alone, and only a read that began before it (one the write's own\n * refresh did not reach) is asked again. An echo that arrives while a write of\n * the page's is still out waits for its answer, so its refresh and the echo\n * agree on which reads are fresh.\n */\n\n/** How long after the page's own write a `self` change is taken to be that write's echo. Short, since the same\n account writing in another tab within it would have its change taken for this page's: a read this page began\n between the two would not be asked again for it (the next change, focus or the counts' clock asks it). */\nconst ECHO_WINDOW_MS = 3_000;\n/** The longest an echo waits for the page's writes still out to be answered. */\nconst ECHO_WAIT_MS = 10_000;\n\nlet out = 0;\nlet answered: { at: number; mark: number } | undefined;\nlet waiting: Array<() => void> = [];\n/* The moment a write was answered, in the order reads begin in (recordWriteRefresh.ts liveMark). */\nlet marker: () => number = () => 0;\n\n/** Where an answered write stands among the reads (recordWriteRefresh.ts installs it). */\nexport function setOwnWriteMarker(mark: () => number): void {\n marker = mark;\n}\n\n/** A write is going out: call what this returns once it is answered, or has failed. */\nexport function ownWriteSent(): () => void {\n out++;\n let done = false;\n return () => {\n if (done) return;\n done = true;\n out--;\n answered = { at: Date.now(), mark: marker() };\n if (out > 0) return;\n const ready = waiting;\n waiting = [];\n for (const resume of ready) resume();\n };\n}\n\n/** Whether a write of this page's is still out. */\nexport const ownWritesOut = (): boolean => out > 0;\n\n/** Once every write of this page's that is out has been answered, or ECHO_WAIT_MS has passed. */\nexport function ownWritesAnswered(): Promise<void> {\n if (out === 0) return Promise.resolve();\n return new Promise((resolve) => {\n const timer = setTimeout(resolve, ECHO_WAIT_MS);\n waiting.push(() => {\n clearTimeout(timer);\n resolve();\n });\n });\n}\n\n/** Where the page's last write was answered, when that was a moment ago; undefined when no write of the page's is that recent. */\nexport function ownWriteMark(): number | undefined {\n return answered && Date.now() - answered.at < ECHO_WINDOW_MS ? answered.mark : undefined;\n}\n\n/** Forget every write: the session or connection ended. */\nexport function forgetOwnWrites(): void {\n answered = undefined;\n}\n","/**\n * The list a record was opened from, carried so the record can step to its\n * neighbors.\n *\n * Every list already hands `openRecord` the records it is showing (the\n * modal's Previous and Next read them); a grid adds its own question and\n * page. That rides the walk entry as `trail`, so a record page, a peek and\n * the split's detail can say \"3 of 20\" and move one record along — in the\n * list's own order and filter, never a second query of their own. Past the\n * page's edge the list's next or previous page is read with the same\n * question; where the list has no total (an API reports none), a full page\n * is the only sign that more follow, as it is for the pager.\n *\n * Pure: the page reader is passed in, so core.js can import this without a\n * cycle and a test needs no server.\n */\n\nexport interface TrailRecord {\n key: Record<string, unknown>;\n label?: string;\n}\n/** The list's question and the page the records came from. `total` only when it is exact. */\nexport interface TrailList {\n query: string;\n offset: number;\n limit: number;\n total?: number;\n}\nexport interface RecordTrail {\n table: string;\n records: TrailRecord[];\n index: number;\n list?: TrailList;\n}\n/** Reads one page of the list's question, as trail records. */\nexport type TrailPageReader = (table: string, list: TrailList, offset: number) => Promise<TrailRecord[]>;\n\n/**\n * The list a room's answer is one page of, so a record opened from any of\n * its presentations — the grid, cards, a queue, a checklist, a split's list\n * — steps past the page with the same question. Undefined where the answer\n * does not say which page it was. An estimate is no end to stop at: without\n * an exact total, a full page is the sign that more follow — nor is a\n * bounded one (\"10,000+\"), which says only where counting stopped.\n */\nexport function trailListFor(\n query: string,\n page: { limit?: number; offset?: number; total?: number; exactTotal?: boolean; cappedTotal?: boolean },\n): TrailList | undefined {\n if (typeof page.limit !== 'number') return undefined;\n return {\n query,\n offset: page.offset ?? 0,\n limit: page.limit,\n ...(page.exactTotal === false || page.cappedTotal || typeof page.total !== 'number' ? {} : { total: page.total }),\n };\n}\n\n/** A key said the same way whatever its column order or value types. */\nexport const keyText = (key: Record<string, unknown>) =>\n JSON.stringify(\n Object.keys(key)\n .sort()\n .map((name) => [name, String(key[name])]),\n );\n\n/**\n * The trail for a record opened from a list, or undefined where there is\n * nothing to step to: the record is not among the list's records, or it is\n * the only one and no page follows.\n */\nexport function trailFor(\n table: string,\n key: Record<string, unknown>,\n records?: readonly TrailRecord[],\n list?: TrailList,\n): RecordTrail | undefined {\n if (!records?.length) return undefined;\n const index = records.findIndex((record) => keyText(record.key) === keyText(key));\n if (index < 0) return undefined;\n const trail: RecordTrail = {\n table,\n index,\n records: records.map((record) => ({ key: { ...record.key }, ...(record.label === undefined ? {} : { label: record.label }) })),\n ...(list ? { list: { ...list } } : {}),\n };\n return trail.records.length > 1 || canStep(trail, 1) || canStep(trail, -1) ? trail : undefined;\n}\n\n/** Where the record sits: its place in the whole list, and the list's length when it is known. */\nexport function trailPosition(trail: RecordTrail): { at: number; of?: number } {\n const at = (trail.list?.offset ?? 0) + trail.index + 1;\n const of = trail.list ? trail.list.total : trail.records.length;\n return of === undefined ? { at } : { at, of };\n}\n\n/** Whether a step that way leads anywhere. */\nexport function canStep(trail: RecordTrail, step: 1 | -1): boolean {\n if (step === -1) return trail.index > 0 || Boolean(trail.list && trail.list.offset > 0);\n if (trail.index < trail.records.length - 1) return true;\n const list = trail.list;\n if (!list) return false;\n // Without a total, a full page is the only sign that another follows.\n return list.total === undefined ? trail.records.length >= list.limit : list.offset + trail.records.length < list.total;\n}\n\n/**\n * The trail one record along within the page, or undefined where the step\n * leaves it. Synchronous, so a step that reads nothing lands at once.\n */\nexport function stepWithin(trail: RecordTrail, step: 1 | -1): RecordTrail | undefined {\n const next = trail.index + step;\n return next >= 0 && next < trail.records.length ? { ...trail, index: next } : undefined;\n}\n\n/**\n * The trail one record along. Within the page it is a move of the index;\n * past it, the list's next (or previous) page is read and the step lands on\n * its first (or last) record. Undefined when there is nowhere to go, which\n * a reader answering an empty page also says.\n */\nexport async function stepTrail(trail: RecordTrail, step: 1 | -1, read: TrailPageReader): Promise<RecordTrail | undefined> {\n const within = stepWithin(trail, step);\n if (within) return within;\n if (!trail.list || !canStep(trail, step)) return undefined;\n const offset = Math.max(0, trail.list.offset + step * trail.list.limit);\n const records = await read(trail.table, trail.list, offset);\n if (!records.length) return undefined;\n return { ...trail, records, index: step === 1 ? 0 : records.length - 1, list: { ...trail.list, offset } };\n}\n","/**\n * Building DOM, and the keyboard manners of the page's own popups: a\n * disclosure that Escape and an outside click close, and a trap that keeps\n * Tab inside a panel and hands the focus back on the way out.\n *\n * Out of core.js, which re-exports all four so every caller keeps its\n * import. Nothing here reads the walk, the schema or the API, so this module\n * imports nothing and core.js can import it without a cycle.\n */\nimport { addHostListener, removeHostListener, shellQuery } from './shellHost.js';\n\nexport function el(tag, attrs = {}, children = []) {\n const node = document.createElement(tag);\n for (const [k, v] of Object.entries(attrs)) {\n if (v === undefined || v === false || v === null) continue;\n if (k === 'class') node.className = v;\n else if (k === 'text') node.textContent = v;\n else if (k.startsWith('on')) node.addEventListener(k.slice(2), v);\n else node.setAttribute(k, v === true ? '' : String(v));\n }\n for (const child of [].concat(children)) {\n if (child == null || child === false) continue;\n node.append(child);\n }\n return node;\n}\n\nexport function svgEl(tag, attrs = {}, children = []) {\n const node = document.createElementNS('http://www.w3.org/2000/svg', tag);\n for (const [k, v] of Object.entries(attrs)) {\n if (v === undefined || v === null) continue;\n if (k.startsWith('on')) node.addEventListener(k.slice(2), v);\n else node.setAttribute(k, String(v));\n }\n for (const child of [].concat(children)) if (child) node.append(child);\n return node;\n}\n\n/**\n * Make a `<details>` announce itself as the control it looks like.\n *\n * A `<summary>` styled as a button is read by Chromium as `group: + Count` —\n * no role, no state, nothing to say it can be opened or that it currently is.\n * The reader sees a button; the screen reader is told about a grouping box.\n * Every disclosure in this app is a button in appearance and in intent, so\n * each one says so, and keeps `aria-expanded` in step with the element's own\n * `open` — through the `toggle` event, because a `<details>` can be opened by\n * the keyboard, by a click on the marker, or by `open` being set in code, and\n * only the event sees all three.\n */\nexport function disclosure(details, { dismissible = true } = {}) {\n const summary = details.querySelector(':scope > summary');\n if (!summary) return details;\n summary.setAttribute('role', 'button');\n summary.setAttribute('aria-expanded', String(details.open));\n /* Light dismissal, here so every disclosure gets it: the Export menu was\n the one popup on the page that Escape and an outside click both left\n open. Escape closes only this layer — stopped, the way every stacked\n surface here learned to — and hands focus back to the summary. */\n const outside = (e) => {\n if (!details.contains(e.target)) details.open = false;\n };\n const onKey = (e) => {\n if (e.key !== 'Escape') return;\n e.preventDefault();\n e.stopPropagation();\n details.open = false;\n summary.focus();\n };\n details.addEventListener('toggle', () => {\n summary.setAttribute('aria-expanded', String(details.open));\n if (!dismissible) return;\n if (details.open) {\n addHostListener(document, 'pointerdown', outside, true);\n addHostListener(document, 'keydown', onKey, true);\n } else {\n removeHostListener(document, 'pointerdown', outside, true);\n removeHostListener(document, 'keydown', onKey, true);\n }\n });\n return details;\n}\n\n/**\n * Keep the keyboard inside a panel, and give it back on the way out.\n *\n * Eight surfaces here declare `role=\"dialog\"` and `aria-modal=\"true\"` on a\n * plain `<div>`. The attribute is a promise: it tells a screen reader that\n * everything behind this is inert and not to be reached. Nothing was\n * enforcing it, so Tab walked straight out of the panel and into the table\n * underneath — the page reading as sealed to one user and open to another,\n * which is worse than not claiming it at all. The `<dialog>` elements\n * elsewhere get this from the platform; these had to be told.\n *\n * It also puts focus back where it came from. Closing a panel used to drop\n * focus on `<body>`, so the next Tab started again from the top of the\n * document — a keyboard user who opened the connection manager from the\n * toolbar and pressed Escape had to walk the whole page to get back.\n *\n * Returns the release function. Call it when the panel goes.\n */\nexport function trapFocus(panel) {\n const cameFrom = document.activeElement;\n\n /* Queried per keystroke rather than cached: these panels rebuild their own\n contents constantly — a filter that repaints a list, a tour that swaps\n the card — and a list captured on open goes stale immediately. */\n const focusable = () =>\n [\n ...panel.querySelectorAll(\n 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]),' +\n ' textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])',\n ),\n ].filter((node) => node.offsetWidth > 0 || node.offsetHeight > 0 || node === document.activeElement);\n\n /* Focus in a modal opened over this panel is that modal's to keep. Since\n Modal.tsx moved every dialog onto Base UI there is no `<dialog open>` to\n see: a modal is an `aria-modal` element on a portal of its own, and this\n trap pulled focus back from it while Base UI's pulled it in again — the\n connection form's name, filled as it opened, landed in the URL box. */\n const aboveUs = (node) => {\n if (shellQuery('dialog[open]')) return true;\n const owner = node instanceof Element ? node.closest('[aria-modal=\"true\"]') : null;\n return Boolean(owner && owner !== panel && !owner.contains(panel));\n };\n\n const onKey = (e) => {\n if (e.key !== 'Tab') return;\n if (aboveUs(document.activeElement)) return;\n const stops = focusable();\n /* Nothing to move to yet — a panel painted but not filled. Swallowing the\n key is the honest answer: the alternative is Tab escaping into the page\n this panel has told a screen reader is inert. Escape still closes. */\n if (!stops.length) {\n e.preventDefault();\n return;\n }\n const first = stops[0];\n const last = stops[stops.length - 1];\n /* Focus outside the panel entirely — it can happen, because a panel may\n be opened while something behind it is focused. Pull it back rather\n than letting the first Tab escape. */\n if (!panel.contains(document.activeElement)) {\n e.preventDefault();\n (e.shiftKey ? last : first).focus();\n return;\n }\n if (e.shiftKey && document.activeElement === first) {\n e.preventDefault();\n last.focus();\n } else if (!e.shiftKey && document.activeElement === last) {\n e.preventDefault();\n first.focus();\n }\n };\n\n /**\n * Focus that ends up outside, brought back.\n *\n * Tab is not the only way out. These panels repaint themselves — the\n * connection list arrives and replaces the loading state, a filter redraws\n * the rows — and replacing the node that had focus drops it on `<body>`,\n * outside the thing that claims to be modal. A click on the page behind\n * does the same.\n *\n * A modal on top is the exception, and it has to be: the connections\n * panel opens one to add a connection, and pulling focus back to the panel\n * underneath would make that dialog impossible to type into. The modal\n * keeps its own focus, so this stands aside.\n */\n const onFocusIn = (e) => {\n if (!panel.isConnected) return;\n if (panel.contains(e.target)) return;\n if (aboveUs(e.target)) return;\n const stops = focusable();\n if (stops.length) stops[0].focus();\n };\n\n /**\n * The panel rebuilding itself out from under the keyboard.\n *\n * This is the one that actually bit. These panels paint twice — a loading\n * state, then the answer — and `replaceChildren` destroys whatever had\n * focus. Removing a focused element does not reliably fire `blur` or\n * `focusout` in Chrome, so nothing above hears it; focus simply becomes\n * `<body>` and the next Tab starts from the top of the document, outside\n * the panel that claims to be modal.\n *\n * Watching the subtree catches it whatever caused it — a repaint, a row\n * that removed itself, a list that filtered. If the panel put focus\n * somewhere itself, as the connection filter does, this sees focus already\n * inside and stands aside.\n */\n const watcher = new MutationObserver(() => {\n if (!panel.isConnected) return;\n if (panel.contains(document.activeElement)) return;\n if (aboveUs(document.activeElement)) return;\n const stops = focusable();\n if (stops.length) stops[0].focus();\n });\n watcher.observe(panel, { childList: true, subtree: true });\n\n addHostListener(document, 'keydown', onKey, true);\n addHostListener(document, 'focusin', onFocusIn);\n return () => {\n watcher.disconnect();\n removeHostListener(document, 'keydown', onKey, true);\n removeHostListener(document, 'focusin', onFocusIn);\n /* Only if the panel still holds it. Something that closed itself by\n sending you somewhere — a connection row that opens that database —\n has already put focus where it belongs. */\n if (cameFrom?.isConnected && !document.activeElement?.closest?.('dialog')) {\n if (cameFrom instanceof HTMLElement && (!panel.isConnected || panel.contains(document.activeElement))) cameFrom.focus();\n }\n };\n}\n","/**\n * Rollups: one number about the rows that point at a record — an order's\n * line total, an account's open deals — declared once on the resource\n * (`rollups: { line_total: sum('order_line', 'amount') }`) and read wherever\n * a column of the record is: a list's `show`, a sort, a tab's condition, a\n * record page's fields, a card's figure and facts, a queue's facts, a\n * picker's query, a dashboard's measure and a breakdown's key — and through\n * a reference, as `sales_order_id.lines`, wherever a path is.\n *\n * A rollup is never a column of the table. It is not in `TableMeta.columns`,\n * so every path that reads one table by its own columns — an insert form, a\n * plain `/api/query`, an export of the table, a filter box built from the\n * schema — never sees it and never sends it to a single-table read, which\n * would be a statement naming a column the database does not have. It lives\n * beside the columns (`TableMeta.rollups`), where the path resolver finds it\n * by name, and it is compiled only by the view compiler, as a correlated\n * scalar subquery through one reference: the same construction as a view's\n * aggregates (viewPaths.ts says why a subquery rather than a grouped join),\n * so a list still has one row per record however many rollups it shows.\n *\n * The child rows are read through the statement's own relations\n * (rowScope.ts), so a row policy or a tenant scope on the child narrows the\n * number exactly as it narrows a related list of the same rows, and a reader\n * whose catalog cannot see the child table, its column or the reference is\n * served no rollup at all (appRollupRules), rather than a number built from\n * rows they may not read.\n */\nimport type { Filter } from './filter.js';\nimport type { ComputedColumn } from './computed.js';\nimport type { ColumnMeta } from './schema.js';\n\nexport const ROLLUP_FNS = ['count', 'sum', 'min', 'max', 'latest'] as const;\nexport type RollupFn = (typeof ROLLUP_FNS)[number];\n\n/** A rollup as the catalog serves it and a view carries it: no request ever supplies one. */\nexport interface Rollup {\n fn: RollupFn;\n /**\n * The reference from the child rows to the record, as `child.column`\n * (`+`-joined for a composite key): endpoints rather than a constraint\n * name, so a layer that renames tables (an App's logical names, a\n * source's prefix) can rewrite it without resolving anything.\n */\n via: string;\n /** The child's column; absent for a count. */\n column?: string;\n /**\n * A `latest`'s order: the child's column whose greatest value is the latest row (its key breaks a tie), and\n * whose `column` the rollup reads — a payment's status, the last visit's outcome. A word about the rows, not a\n * figure, so it is never summed. Through two references it is the inner level's: the outer reads the newest of\n * all the rows joined to their middle record, not the newest of each middle's newest (viewLabels.ts).\n */\n by?: string;\n /**\n * A `latest` that reads a reference column (the newest lease's `resident_id`): the record it points at, so the value\n * is drawn as that record's name and link and never as its key (referenceModel.ts).\n */\n references?: { table: string; column: string };\n /**\n * Set only on a rollup a path reads *through* (`resident.name`, viewPaths.ts): the value wanted is this column of the\n * record the rollup's key names, read through that table's own relation, so a row policy narrows it as it narrows a\n * join to the same record. The reader must be able to read both (rollupReads).\n */\n through?: { table: string; key: string; column: string };\n /** A `latest`'s vocabulary: the child column's own, so the word it reads is drawn, filtered and typed as that column is. */\n allowed?: string[];\n /**\n * Set when `column` names a computed column of the child (computed.ts)\n * rather than a stored one: its expression, constants folded, written\n * inline over the child's row inside the aggregate — the same SQL a list\n * of the child writes for it. It reads the child's own columns only, never\n * a rollup or a reference (appRollupRules refuses those), so a rollup over\n * it is still one subquery, and no rollup can come to read itself.\n */\n expression?: Pick<ComputedColumn, 'type' | 'expr' | 'reads'>;\n /** Which child rows count — the rollup's `query`, over the child's own columns and paths through its references. */\n filter?: Filter;\n /**\n * The computed columns of the child that `filter` names (`breached = true`), each its expression, constants\n * and `now` folded: written over the row inside the narrowed relation, as `expression` is inside the aggregate.\n * Each reads the child's own columns only (appRollupRules), so the figure is still one subquery.\n */\n computed?: Record<string, Pick<ComputedColumn, 'type' | 'expr' | 'reads'>>;\n /**\n * The tables a path in `filter` walks to, with the columns read there (`sales_order_id.status` reads\n * `sales_order.id` and `sales_order.status`): what a reader must be able to read for the figure to be\n * served (rollupReads), as the child's own columns are.\n */\n reach?: Array<{ table: string; fields: string[] }>;\n /** The value's type: an integer for a count, the column's own otherwise. */\n type: string;\n /**\n * Set when `column` names a rollup of the child rather than one of its\n * columns — a customer's `lines: sum('sales_order', 'lines')`, the count\n * of its orders' lines, which is how a rollup reaches two hops. The inner\n * rollup is compiled inside the outer one's aggregate, each through its own\n * child's relation, so a row policy narrows both levels. One level only:\n * the inner one reads a column (appRollupRules says so when it does not).\n *\n * A rollup whose `whose` walks two references (`sum('time_entry', 'hours',\n * { whose: 'task_id.project_id' })`) is served in the same shape: the outer\n * reads the middle table (`task.project_id`) with no column of its own,\n * the inner the rows and column declared, with the declared query. A count\n * so built is the sum of the inner counts, and 0 where there are none; a\n * max of maxes and a min of mins are the max and min of the rows; a latest\n * is one ordered read over the rows joined to the middle table. The\n * access layer holds a reader to both levels' tables and fields\n * (rollupReads), as it does a rollup of a rollup.\n */\n of?: Rollup;\n}\n\n/**\n * A rollup a view names, keyed by its path in `ViewDef.computed`: the rollup\n * and the path prefix of the record it hangs off ('' for the view's base).\n */\nexport interface ComputedPath extends Rollup {\n on: string;\n}\n\n/** A rollup read as a column: never written, null only where there are no rows to read (a count is 0). */\nexport function rollupColumnMeta(name: string, rollup: Pick<Rollup, 'fn' | 'type' | 'allowed' | 'references'>): ColumnMeta {\n return {\n name,\n type: rollup.type,\n nullable: rollup.fn !== 'count',\n primaryKey: false,\n writable: false,\n ...(rollup.allowed ? { allowed: rollup.allowed } : {}),\n ...(rollup.references ? { references: rollup.references } : {}),\n };\n}\n\n/** The child table a rollup's `via` names. */\nfunction rollupChild(via: string): string {\n return via.slice(0, via.lastIndexOf('.'));\n}\n\n/** `via` with its child table renamed — a layer mapping logical names to physical ones. */\nfunction renamedVia(via: string, rename: (table: string) => string): string {\n const dot = via.lastIndexOf('.');\n return `${rename(via.slice(0, dot))}${via.slice(dot)}`;\n}\n\n/** A rollup with every child table it reads renamed, the inner one's too. */\nfunction renamedRollup<R extends Rollup>(rollup: R, rename: (table: string) => string): R {\n return {\n ...rollup,\n via: renamedVia(rollup.via, rename),\n ...(rollup.reach ? { reach: rollup.reach.map((read) => ({ ...read, table: rename(read.table) })) } : {}),\n ...(rollup.references ? { references: { ...rollup.references, table: rename(rollup.references.table) } } : {}),\n ...(rollup.through ? { through: { ...rollup.through, table: rename(rollup.through.table) } } : {}),\n ...(rollup.of ? { of: renamedRollup(rollup.of, rename) } : {}),\n };\n}\n\n/** A view's or a breakdown's `computed` with every child table renamed. */\nexport function renamedComputed<T extends { computed?: Record<string, ComputedPath> }>(def: T, rename: (table: string) => string): T {\n return def.computed\n ? { ...def, computed: Object.fromEntries(Object.entries(def.computed).map(([path, rollup]) => [path, renamedRollup(rollup, rename)])) }\n : def;\n}\n\n/**\n * Every table a rollup reads rows of, with the fields it reads there: the\n * reference it walks, the column it sums (not an inner rollup's name, which\n * is no field, nor a computed column's, whose own reads stand for it) and\n * the columns its query narrows by — the inner rollup's too. What an access\n * layer holds a reader's grants against.\n */\nexport function rollupReads(rollup: Rollup): Array<{ table: string; fields: string[] }> {\n const table = rollupChild(rollup.via);\n const fields = [\n ...rollup.via.slice(table.length + 1).split('+'),\n ...(rollup.expression ? rollup.expression.reads : rollup.column && !rollup.of ? [rollup.column] : []),\n ...(rollup.by === undefined ? [] : [rollup.by]),\n // A path's first hop is the child's own reference column; the rest is in `reach`.\n ...(rollup.filter?.groups ?? []).flatMap((group) =>\n group.flatMap((c) =>\n [c.column, ...(c.other === undefined ? [] : [c.other])].flatMap((name) =>\n // A computed column's own reads stand for it, as `expression`'s do.\n rollup.computed && Object.hasOwn(rollup.computed, name) ? rollup.computed[name].reads : name.split('.')[0].split('+'),\n ),\n ),\n ),\n ];\n return [\n { table, fields },\n ...(rollup.reach ?? []),\n ...(rollup.of ? rollupReads(rollup.of) : []),\n ...(rollup.through ? [{ table: rollup.through.table, fields: [rollup.through.key, rollup.through.column] }] : []),\n ];\n}\n","/*\n * The view model and its reference paths: what a view holds, and how a path\n * or an aggregate resolves against the foreign-key graph to the tables and\n * columns it names. view.ts compiles what resolves here into SQL.\n */\nimport { type AnyRows, type Op } from './filter.js';\nimport { findTable, isJsonType, referencesFrom, type ColumnMeta, type DatabaseSchema, type ForeignKey, type TableMeta } from './schema.js';\nimport { nearest, shortList } from './suggest.js';\nimport { LABEL_SEGMENT } from './recordLabel.js';\nimport { rollupColumnMeta, type ComputedPath, type Rollup } from './rollups.js';\nimport { computedColumnMeta, type ComputedColumn, type ComputedPath as ExpressionPath } from './computed.js';\nimport { TAGS_COLUMN, tagsColumnMeta, type TagPath, type TagSet } from './tagLists.js';\nimport { DEFINED_KEY, definedColumnMeta } from './definedFields.js';\n\n/* ---------- the model ---------- */\n\n/** One column of a view: a path, and optionally what to call it. */\nexport interface ViewColumn {\n path: string;\n /** Output name. Defaults to the path itself, which is already unique. */\n alias?: string;\n}\n\n/** One condition. Identical to `Condition` except that it addresses a path. */\nexport interface ViewCondition {\n path: string;\n op: Op;\n value?: string | number | boolean | Array<string | number>;\n /** As on `Condition`: compare by bytes; only comparisons take it. */\n collation?: 'binary';\n /** As on `Condition`: compare with this path's value rather than `value`. */\n other?: string;\n /**\n * As on `Condition`: the rows that point at this one, asked whether any\n * matches. `path` is then the condition as it is written (`any opportunity\n * where stage = won`), for a reader to say back; the compiler reads `any`.\n */\n any?: AnyRows;\n}\n\n/** OR-of-ANDs over paths — the same shape as `Filter`, one level out. */\nexport interface ViewFilter {\n groups: ViewCondition[][];\n}\n\nexport interface ViewOrder {\n path: string;\n direction: 'asc' | 'desc';\n /**\n * When true, `path` names an aggregate's output alias rather than a\n * column: `sort invoices desc` after `count invoice … as invoices`.\n * Compiled as the quoted alias, which every dialect here accepts in an\n * ORDER BY — the one clause that runs late enough to see it.\n */\n alias?: boolean;\n /**\n * Closest first: order by the distance of `path` from this number rather\n * than by `path` itself — a similar list's nearest price. Written into the\n * ORDER BY as a literal, so it must be a finite, safe number; anything\n * else is refused, never quoted.\n */\n near?: number;\n /**\n * Order text by its bytes, not by the column's collation, so that a keyset\n * cursor's comparison (`collation` on a condition) agrees with the order it\n * resumes. A column only: an alias or a distance is refused with it.\n */\n collation?: 'binary';\n /**\n * Nulls after every value, in either direction, on every dialect: the order\n * a keyset cursor over a nullable column resumes (apiReadBoundary).\n */\n nulls?: 'last';\n}\n\n/* ---------- the other direction ----------\n\n A reference path walks *forwards*, along a foreign key from the row that\n holds it, so it always lands on at most one row. That is what lets a view\n promise one row per base record.\n\n The backwards direction — the rows that point *at* you — is the more\n interesting question and the one the tool is built around, but it is\n many-valued, and joining it multiplies: a work order with four parts and\n three notes becomes twelve rows, each repeating the work order's own\n columns, and the number on screen stops meaning work orders. Almost nobody\n asking for \"work orders with their parts\" wants that.\n\n An aggregate is the honest one-row-per-record answer to a many-valued\n question. It compiles to a correlated scalar subquery, which is worth\n preferring over a grouped join for three reasons: cardinality cannot change\n no matter how many aggregates are added, each one is independent so two\n counts over different children cannot inflate each other, and it carries no\n parameters — so the placeholder numbering that Postgres cares about is\n untouched by however many of these a view collects. */\n\nexport type AggregateFn = 'count' | 'sum' | 'min' | 'max' | 'avg';\n\nexport interface ViewAggregate {\n fn: AggregateFn;\n /**\n * The foreign key pointing back at the parent, by constraint name.\n *\n * A name rather than a table, because a table can point at the same parent\n * more than once — `work_order.parent_id` and `work_order.supersedes_id`\n * are two different questions and \"count of work_orders\" is neither.\n */\n via: string;\n /** Path prefix naming the parent the children point at. Empty is the base. */\n on?: string;\n /** Column on the child table. Required for everything except `count`. */\n column?: string;\n alias?: string;\n}\n\nexport interface ViewDef {\n id: string;\n name: string;\n /** Base table id. Every path in the view starts here. */\n base: string;\n columns: ViewColumn[];\n /** Scalar summaries of many-valued relationships. */\n aggregates?: ViewAggregate[];\n filter?: ViewFilter;\n orderBy?: ViewOrder[];\n limit?: number;\n /**\n * The view written down, in the same language.\n *\n * When present it is the definition and the structure above is derived from\n * it — a config file holding `work_order show reference, customer_id.name\n * as customer count work_order_part via work_order_id as parts` is a file\n * someone can review in a pull request, which a nest of JSON objects with\n * `op: \"contains\"` in it is not.\n */\n text?: string;\n /** Config-defined views may name the connection they belong to. */\n connection?: string;\n /** Where this came from. Absent means the client's own storage. */\n source?: 'config' | 'saved';\n /**\n * The record-label templates its `$label` paths compile from, by the path\n * prefix each hangs off ('' for the base's own). Filled by the server's\n * read from the App's catalog (recordLabel.ts); a request never supplies it.\n */\n labels?: Record<string, string>;\n /**\n * The rollups its paths name (rollups.ts), by path: what makes `line_total`\n * or `customer_id.open_deals` a correlated subquery rather than a column.\n * Filled by the server's read from the App's catalog, like `labels`, and\n * carried with the view so a layer whose catalog is the database's own\n * (an adapter, a source) compiles the same statement; a request never\n * supplies it.\n */\n computed?: Record<string, ComputedPath>;\n /**\n * The computed columns its paths name (computed.ts), by path, each with its\n * expression and the prefix of the record it is on: filled from the App's\n * catalog like `computed`, so an adapter writes the same expression; a\n * request never supplies it.\n */\n expressions?: Record<string, ExpressionPath>;\n /**\n * The record tags its paths name (tagLists.ts), by path: filled from the\n * App's catalog like `computed`, so an adapter writes the same subquery; a\n * request never supplies them.\n */\n tagged?: Record<string, TagPath>;\n /**\n * The App's declared references (soft keys) the view may walk, as edges of\n * the catalog it is compiled against (declaredReferences.ts). Attached by\n * the App's resource layer, where a catalog that is the database's own\n * does not have them; a request never supplies them.\n */\n references?: ForeignKey[];\n}\n\n/**\n * How many foreign keys a path may follow.\n *\n * A cap is not a nicety: `work_order.parent_id` points at `work_order`, so the\n * path space is infinite and any breadth-first enumeration of it runs forever.\n * Four hops is past the point where anyone can still read the result, and the\n * limit is stated in the error rather than left to be discovered.\n */\nexport const MAX_PATH_DEPTH = 4;\n\n/** What a `$label` path ends on: text, and empty where the row is missing. */\nexport const labelColumnMeta = (): ColumnMeta => ({ name: LABEL_SEGMENT, type: 'TEXT', nullable: true, primaryKey: false });\n\n/* ---------- errors ---------- */\n\n/**\n * A path that does not resolve, with the segment that failed.\n *\n * The segment matters more than the path: `customer_id.country_code.nmae` is\n * four words of which one is wrong, and an error that repeats the whole path\n * makes you find the wrong one yourself.\n */\nexport class PathError extends Error {\n constructor(\n message: string,\n readonly path: string,\n readonly segment: string,\n /** Index of the failing segment within the path, 0-based. */\n readonly index: number,\n ) {\n super(message);\n this.name = 'PathError';\n }\n}\n\n/* ---------- hops ---------- */\n\n/**\n * What a hop is called in a path.\n *\n * A single-column foreign key is named by its column, which is what anyone\n * would write and what the composer shows. A composite key has no single\n * column to name it, so its columns are joined with `+` — deterministic,\n * derivable from the model alone, and impossible to confuse with a column\n * name because `+` is not legal in one.\n */\nexport function hopName(fk: ForeignKey): string {\n return fk.from.columns.length === 1 ? fk.from.columns[0] : fk.from.columns.join('+');\n}\n\nexport interface Hop {\n /** What to write in a path. */\n name: string;\n fk: ForeignKey;\n /** The table this hop lands on. */\n target: string;\n /** True when every column of the key is nullable — the row may not exist. */\n optional: boolean;\n}\n\n/** Every forward hop available from a table, in a stable order. */\nexport function hopsFrom(schema: DatabaseSchema, tableId: string): Hop[] {\n const table = findTable(schema, tableId);\n return referencesFrom(schema, tableId).map((fk) => ({\n name: hopName(fk),\n fk,\n target: fk.to.table,\n optional: fk.from.columns.some((c) => table?.columns.find((col) => col.name === c)?.nullable !== false),\n }));\n}\n\n/** A table's rollup of that name where it is a latest of a reference column: the record it names can be read through it. */\nfunction latestReference(table: TableMeta, name: string): Rollup | undefined {\n const rollup = table.rollups && Object.hasOwn(table.rollups, name) ? table.rollups[name] : undefined;\n return rollup?.fn === 'latest' && rollup.references && !rollup.of ? rollup : undefined;\n}\n\nexport interface ResolvedPath {\n path: string;\n /** The foreign keys followed, in order. Empty for a plain column. */\n hops: ForeignKey[];\n hopNames: string[];\n /** The table the final column lives on. */\n table: TableMeta;\n column: ColumnMeta;\n /** Set when the path ends on a rollup of `table` rather than a column (rollups.ts). */\n rollup?: Rollup;\n /** With `rollup`: the prefix of the record the rollup is on, where that is not the path's own (a path read through the rollup, below). */\n rollupAt?: string;\n /** Set when the path ends on a computed column of `table` (computed.ts). */\n computed?: ComputedColumn;\n /** Set when the path ends on the record's tags (tagLists.ts). */\n tags?: TagSet;\n /** Set when the path reads one key of a JSON column (`specs.capacity`, definedFields.ts): the column and the key. */\n jsonKey?: { column: string; key: string };\n}\n\n/** The type of the column a path lands on, where it is that column itself: undefined for a rollup, a computed column,\n * a record's tags or a JSON key, which are expressions, and for a path that does not resolve. */\nexport function plainType(schema: DatabaseSchema, base: string, path: string): string | undefined {\n const { resolved } = tryResolvePath(schema, base, path);\n if (!resolved || resolved.rollup || resolved.computed || resolved.tags || resolved.jsonKey) return undefined;\n return resolved.column.type;\n}\n\n/**\n * Resolve a path against the schema, or explain precisely why it does not.\n *\n * Every hop is checked against the foreign key graph, so an unknown segment is\n * an error here rather than a database exception later — or, far worse, a\n * silently empty result that looks like a table with nothing in it.\n */\nexport function resolvePath(schema: DatabaseSchema, baseId: string, path: string): ResolvedPath {\n /* A path means the same against the same catalog, and a list asks for each\n of its columns' paths on every request, per row's worth of fields. Kept\n per schema object, so a reloaded App or a new catalog starts empty; a\n refused path is not kept (it throws, and is the rare one). The result is\n frozen, shared by every caller. */\n let kept = resolved.get(schema);\n if (!kept) resolved.set(schema, (kept = new Map()));\n const key = `${baseId}\\u0000${path}`;\n const hit = kept.get(key);\n if (hit) return hit;\n const made = resolvePathFresh(schema, baseId, path);\n Object.freeze(made.hops);\n Object.freeze(made.hopNames);\n // A reader can type paths (a query's `a.b.c`), and a path may loop through\n // the graph, so what is kept is bounded; the declared ones come straight back.\n if (kept.size >= MAX_KEPT_PATHS) kept.clear();\n kept.set(key, Object.freeze(made));\n return made;\n}\n\nconst resolved = new WeakMap<DatabaseSchema, Map<string, ResolvedPath>>();\nconst MAX_KEPT_PATHS = 2048;\n\nfunction resolvePathFresh(schema: DatabaseSchema, baseId: string, path: string): ResolvedPath {\n const raw = String(path ?? '').trim();\n if (!raw) throw new PathError('A path cannot be empty.', raw, '', 0);\n\n const segments = raw.split('.');\n const blank = segments.findIndex((s) => s.trim() === '');\n if (blank !== -1) {\n throw new PathError(`\"${raw}\" has an empty segment — two dots in a row, or a trailing dot.`, raw, '', blank);\n }\n\n const hopCount = segments.length - 1;\n if (hopCount > MAX_PATH_DEPTH) {\n throw new PathError(\n `\"${raw}\" follows ${hopCount} references; at most ${MAX_PATH_DEPTH} are allowed. ` +\n 'Deeper walks are usually two views rather than one.',\n raw,\n segments[MAX_PATH_DEPTH],\n MAX_PATH_DEPTH,\n );\n }\n\n let table = findTable(schema, baseId);\n if (!table) {\n throw new PathError(`No table called \"${baseId}\" in this database.`, raw, segments[0], 0);\n }\n\n const hops: ForeignKey[] = [];\n const hopNames: string[] = [];\n\n for (let i = 0; i < segments.length - 1; i++) {\n const segment = segments[i];\n const available = hopsFrom(schema, table.id);\n const matches = available.filter((h) => h.name === segment || h.fk.name === segment);\n /* The last step after a latest that reads a reference is a column of the record it names (`resident.name`): the\n rollup's key, read through that table's own relation (viewLabels.ts), so a list draws the name beside the key. */\n const through = !matches.length && i === segments.length - 2 ? latestReference(table, segment) : undefined;\n if (through) {\n const target = findTable(schema, through.references!.table);\n const wanted = segments[i + 1];\n const read = target?.columns.find((c) => c.name === wanted);\n if (!target || !read) {\n throw new PathError(`\"${wanted}\" is not a column on ${target?.name ?? through.references!.table}.`, raw, wanted, i + 1);\n }\n return {\n path: raw,\n hops,\n hopNames,\n table,\n column: read,\n rollup: { ...through, through: { table: target.id, key: through.references!.column, column: read.name } },\n rollupAt: hopNames.join('.'),\n };\n }\n /* The last step after a JSON column is one of its keys (`specs.capacity`):\n a value the definition rows name (definedFields.ts), read, filtered and\n sorted like a column. A reference of the same name wins. */\n const json =\n !matches.length && i === segments.length - 2 ? table.columns.find((c) => c.name === segment && isJsonType(c.type)) : undefined;\n if (json) {\n const key = segments[i + 1];\n if (!DEFINED_KEY.test(key))\n throw new PathError(`\"${key}\" is not a key \"${segment}\" can hold; a key is a name of letters, digits and _.`, raw, key, i + 1);\n return { path: raw, hops, hopNames, table, column: definedColumnMeta(key), jsonKey: { column: json.name, key } };\n }\n if (!matches.length) {\n /* A real column that simply is not a reference is the commonest\n mistake, and deserves its own sentence: \"total is not a reference\"\n beats \"total is not one of customer_id, sold_by\". */\n const isColumn = table.columns.some((c) => c.name === segment);\n const names = available.map((h) => h.name);\n const suggestion = nearest(segment, names);\n const hint = suggestion\n ? ` Did you mean \"${suggestion}\"?`\n : names.length\n ? ` ${table.name} references: ${shortList(names)}.`\n : ` ${table.name} has no foreign keys.`;\n throw new PathError(\n isColumn\n ? `\"${segment}\" is a column on ${table.name}, not a reference, so nothing can follow it.${hint}`\n : `\"${segment}\" is not a reference on ${table.name}.${hint}`,\n raw,\n segment,\n i,\n );\n }\n if (matches.length > 1) {\n throw new PathError(\n `\"${segment}\" is ambiguous on ${table.name} — ${matches.length} foreign keys match. ` +\n `Name the constraint instead: ${shortList(matches.map((m) => m.fk.name))}.`,\n raw,\n segment,\n i,\n );\n }\n const next = findTable(schema, matches[0].target);\n if (!next) {\n throw new PathError(`\"${segment}\" points at \"${matches[0].target}\", which is not in this schema.`, raw, segment, i);\n }\n hops.push(matches[0].fk);\n hopNames.push(matches[0].name);\n table = next;\n }\n\n const last = segments[segments.length - 1];\n if (last === LABEL_SEGMENT) {\n if (!table.labelTemplate) {\n throw new PathError(\n `${table.name} has no record label template, so \"${LABEL_SEGMENT}\" names nothing on it.`,\n raw,\n last,\n segments.length - 1,\n );\n }\n return { path: raw, hops, hopNames, table, column: labelColumnMeta() };\n }\n const column = table.columns.find((c) => c.name === last) ?? table.columns.find((c) => c.name.toLowerCase() === last.toLowerCase());\n /* A rollup is found by its exact name only, and after the columns: the\n catalog never serves one that shares a column's name, so this order\n only matters to a hand-made catalog, where the column wins. */\n const rollup = column ? undefined : table.rollups && Object.hasOwn(table.rollups, last) ? table.rollups[last] : undefined;\n if (rollup) return { path: raw, hops, hopNames, table, column: rollupColumnMeta(last, rollup), rollup };\n const computed = column ? undefined : table.computed && Object.hasOwn(table.computed, last) ? table.computed[last] : undefined;\n if (computed) return { path: raw, hops, hopNames, table, column: computedColumnMeta(last, computed), computed };\n if (!column && last === TAGS_COLUMN && table.tags)\n return { path: raw, hops, hopNames, table, column: tagsColumnMeta(), tags: table.tags };\n if (!column) {\n const suggestion = nearest(\n last,\n table.columns.map((c) => c.name),\n );\n throw new PathError(\n `\"${last}\" is not a column on ${table.name}.${suggestion ? ` Did you mean \"${suggestion}\"?` : ''}`,\n raw,\n last,\n segments.length - 1,\n );\n }\n\n return { path: raw, hops, hopNames, table, column };\n}\n\n/* ---------- many-valued relationships ---------- */\n\ninterface Backref {\n /** What to write in an aggregate's `via`. */\n name: string;\n fk: ForeignKey;\n /** The table holding the key — the \"many\" end. */\n child: string;\n}\n\n/** Every foreign key pointing at a table: the \"what refers to me\" direction. */\nexport function backrefsTo(schema: DatabaseSchema, tableId: string): Backref[] {\n return schema.foreignKeys.filter((fk) => fk.to.table === tableId).map((fk) => ({ name: fk.name, fk, child: fk.from.table }));\n}\n\ninterface ResolvedAggregate {\n aggregate: ViewAggregate;\n fk: ForeignKey;\n /** The table the children live in. */\n child: TableMeta;\n /** The table the children point at. */\n parent: TableMeta;\n /** The path prefix of the parent, '' for the base table. */\n on: string;\n column?: ColumnMeta;\n /** Default output name, e.g. `count(work_order_part)`. */\n name: string;\n}\n\nexport const AGGREGATE_FNS: AggregateFn[] = ['count', 'sum', 'min', 'max', 'avg'];\n\nexport function resolveAggregate(schema: DatabaseSchema, baseId: string, aggregate: ViewAggregate): ResolvedAggregate {\n const on = (aggregate.on ?? '').trim();\n const fail = (message: string, segment: string) => {\n throw new PathError(message, aggregate.via, segment, 0);\n };\n\n if (!AGGREGATE_FNS.includes(aggregate.fn)) {\n fail(`\"${String(aggregate.fn)}\" is not a summary. Use one of: ${AGGREGATE_FNS.join(', ')}.`, String(aggregate.fn));\n }\n\n /* The parent is the base table, or whatever a path prefix reaches. Resolved\n by resolving a path *through* it — the same code, so a prefix that is not\n a real chain of keys fails here exactly as it would anywhere else. */\n const parent = tableAtPrefix(schema, baseId, on);\n if (!parent) {\n fail(on ? `\"${on}\" is not a chain of references from ${baseId}.` : `No table called \"${baseId}\" in this database.`, on || baseId);\n }\n\n const candidates = backrefsTo(schema, parent!.id);\n // A qualified relationship spelling identifies endpoints, not a constraint.\n // Prefer it when a physical constraint happens to have the same text; App\n // logical-to-physical transfers use this form to preserve the selected edge.\n const match =\n candidates.find((b) => `${b.child}.${b.fk.from.columns.join('+')}` === aggregate.via) ??\n candidates.find((b) => b.name === aggregate.via);\n if (!match) {\n fail(\n `Nothing called \"${aggregate.via}\" points at ${parent!.name}.` +\n (candidates.length ? ` It is referred to by: ${shortList(candidates.map((c) => c.name))}.` : ' Nothing refers to it.'),\n aggregate.via,\n );\n }\n\n const child = findTable(schema, match!.child);\n if (!child) fail(`\"${match!.child}\" is not in this schema.`, aggregate.via);\n\n let column: ColumnMeta | undefined;\n if (aggregate.fn !== 'count') {\n if (!aggregate.column) {\n fail(`${aggregate.fn} needs a column on ${child!.name} to summarise.`, aggregate.via);\n }\n column = child!.columns.find((c) => c.name === aggregate.column);\n if (!column) {\n const suggestion = nearest(\n aggregate.column!,\n child!.columns.map((c) => c.name),\n );\n fail(\n `\"${aggregate.column}\" is not a column on ${child!.name}.${suggestion ? ` Did you mean \"${suggestion}\"?` : ''}`,\n aggregate.column!,\n );\n }\n }\n\n const subject = `${on ? `${on}.` : ''}${child!.name}${column ? `.${column.name}` : ''}`;\n return {\n aggregate,\n fk: match!.fk,\n child: child!,\n parent: parent!,\n on,\n column,\n name: aggregate.alias?.trim() || `${aggregate.fn}(${subject})`,\n };\n}\n\n/** The table a chain of hop names lands on. Empty prefix is the base itself. */\nexport function tableAtPrefix(schema: DatabaseSchema, baseId: string, prefix: string): TableMeta | undefined {\n let table = findTable(schema, baseId);\n if (!prefix) return table;\n for (const segment of prefix.split('.')) {\n if (!table) return undefined;\n const hop = hopsFrom(schema, table.id).find((h) => h.name === segment || h.fk.name === segment);\n if (!hop) return undefined;\n table = findTable(schema, hop.target);\n }\n return table;\n}\n\n/** Resolve without throwing — for callers collecting several errors at once. */\nexport function tryResolvePath(schema: DatabaseSchema, baseId: string, path: string): { resolved?: ResolvedPath; error?: PathError } {\n try {\n return { resolved: resolvePath(schema, baseId, path) };\n } catch (err) {\n if (err instanceof PathError) return { error: err };\n throw err;\n }\n}\n","/*\n * Row scopes: the per-resource row filters a server-owned policy supplies,\n * captured within fixed limits, and the scoped relations a read or write is\n * compiled against. Shared so views and breakdowns compile under a scope\n * through the same builder as a plain read.\n */\nimport { buildWhere, OPS, type Filter, type SqlFragment, type SqlStyle } from './filter.js';\nimport { Refusal } from './refusal.js';\nimport { findTable, type DatabaseSchema } from './schema.js';\nimport { isFixedScopeCatalog } from './scopeCatalog.js';\nimport { qualifiedTable } from './sqlRelation.js';\nimport { plainType, tryResolvePath } from './viewPaths.js';\n\n/** Internal, server-owned compiler input; never a member of a request definition.\n * Exact catalog IDs only. With a scope present, missing resources deny rows.\n * `true` explicitly allows all rows; filters address local columns, not paths.\n * This primitive alone does not enable App authorization or tenant isolation.\n */\nexport interface RowScopeFilter extends Filter {\n /** Private SQLite comparison metadata, never SQL or a request filter field.\n * Applies BINARY collation to these local columns; LIKE keeps its own semantics. */\n readonly binary?: readonly string[];\n}\nexport type RowScope = Readonly<Record<string, boolean | RowScopeFilter>>;\n\n/** Composition must budget expanded data against the same capture limits. */\nexport const ROW_SCOPE_LIMITS = Object.freeze({\n resources: 256,\n conditions: 2048,\n array: 4096,\n nodes: 16_384,\n string: 16_384,\n characters: 262_144,\n});\n\nconst invalid = (): never => {\n throw new Refusal('Invalid row scope.');\n};\n\n/** Copy before validation so accessors, prototypes and later mutation cannot\n * change what was checked. Bounds apply to the entire scope, not each table.\n * Frozen nested values stay private; Readonly<Record> alone is not a snapshot.\n */\nexport function captureRowScope(input: RowScope): RowScope {\n let nodes = 0;\n let characters = 0;\n const copy = (value: unknown, depth = 0): unknown => {\n if (++nodes > ROW_SCOPE_LIMITS.nodes || depth > 8) return invalid();\n if (typeof value === 'string') {\n characters += value.length;\n if (value.length > ROW_SCOPE_LIMITS.string || characters > ROW_SCOPE_LIMITS.characters) return invalid();\n return value;\n }\n if (typeof value === 'boolean' || (typeof value === 'number' && Number.isFinite(value))) return value;\n if (!value || typeof value !== 'object') return invalid();\n const array = Array.isArray(value);\n const proto = Object.getPrototypeOf(value);\n if (array ? proto !== Array.prototype : proto !== Object.prototype && proto !== null) return invalid();\n const keys = Reflect.ownKeys(value);\n if (keys.length > ROW_SCOPE_LIMITS.array + 1) return invalid();\n if (array) {\n const length = Object.getOwnPropertyDescriptor(value, 'length')?.value;\n if (!Number.isInteger(length) || length > ROW_SCOPE_LIMITS.array || keys.length !== length + 1) return invalid();\n const result: unknown[] = [];\n for (let i = 0; i < length; i++) {\n const descriptor = Object.getOwnPropertyDescriptor(value, String(i));\n if (!descriptor || !('value' in descriptor) || !descriptor.enumerable) return invalid();\n result.push(copy(descriptor.value, depth + 1));\n }\n return Object.freeze(result);\n }\n const result: Record<string, unknown> = Object.create(null);\n for (const key of keys) {\n if (typeof key !== 'string') return invalid();\n characters += key.length;\n if (key.length > ROW_SCOPE_LIMITS.string || characters > ROW_SCOPE_LIMITS.characters) return invalid();\n const descriptor = Object.getOwnPropertyDescriptor(value, key)!;\n if (!('value' in descriptor) || !descriptor.enumerable) return invalid();\n result[key] = copy(descriptor.value, depth + 1);\n }\n return Object.freeze(result);\n };\n const scope = copy(input);\n if (!scope || typeof scope !== 'object' || Array.isArray(scope) || Object.keys(scope).length > ROW_SCOPE_LIMITS.resources)\n return invalid();\n return scope as RowScope;\n}\n\n/* The scopes this module copied, froze and checked, each with the catalogs it\n was checked against. A tenant read snapshotted one request's scope again at\n every layer it crossed — composed, authorized, bound, and once more per\n statement — each a full copy and a scan of the catalog for every resource:\n a field report's profile put it among the largest costs of a page. A\n snapshot is frozen all the way down and was made here, never by a caller,\n so checking it again against the same catalog object could only pass and\n copy it to an equal value: it is handed back as it is. A catalog that can\n still change is checked afresh every time, as before; anything a caller\n built, however frozen, is copied every time and then found by its text\n (below), never handed back as itself. */\nconst snapshots = new WeakMap<object, WeakSet<DatabaseSchema>>();\nconst checkedAgainst = (scope: object, schema: DatabaseSchema) => snapshots.get(scope)?.has(schema) === true;\n\n/* And by what they say. A policy that names its reader (`owner_account_id = <the account>`) builds its scope afresh\n on every request: a new object each time, equal to the last, so every request copied and checked it again, and\n every cache keyed by a scope's identity downstream (tenantReadScope.ts compositions, the scoped reads' kept\n statements) missed, composing and compiling afresh too — a field report's tenant App, whose staff scope names\n two hundred tables and the reader, spent a sixth of a tenant enter there. The copy is still made, so nothing the\n caller holds can change what is checked; it is then looked up by its text against a catalog nothing can change,\n and an equal scope already checked against it is handed back. The text is the copy's own — plain strings,\n booleans, finite numbers, arrays and records, nothing else survives captureRowScope — so equal text is an equal\n scope. Bounded per catalog; a reader past the bound is checked as before. */\nconst byText = new WeakMap<DatabaseSchema, Map<string, RowScope>>();\nconst BY_TEXT_HELD = 256;\n\n/** A scope's text, where the text says all of it: undefined for anything JSON would say the same as something else\n * (an undefined value, which it drops; a non-finite number; an object that is not a plain record). Only for data\n * nothing can intercept — a copy this module or the tenant capture made — never a caller's live object, whose\n * accessors JSON.stringify would run. */\nexport function rowScopeText(value: unknown): string | undefined {\n let plain = true;\n const text = JSON.stringify(value, (_key, item: unknown) => {\n if (\n item === undefined ||\n (typeof item === 'number' && !Number.isFinite(item)) ||\n (item !== null && typeof item === 'object' && !Array.isArray(item) && ![null, Object.prototype].includes(Object.getPrototypeOf(item)))\n )\n plain = false;\n return item;\n });\n return plain ? text : undefined;\n}\n\n/** Test-only: how many row scopes were copied and checked in full rather than handed back. */\nexport const rowScopeCounters = { checked: 0 };\n\n/** Whether `value` is a row scope snapshotRowScope made: plain data, frozen all the way down, that nothing can change. */\nexport function isRowScopeSnapshot(value: unknown): boolean {\n return value !== null && typeof value === 'object' && snapshots.has(value);\n}\n\n/** Capture can precede an async catalog load; catalog validation cannot. */\nexport function snapshotRowScope(schema: DatabaseSchema, input: RowScope): RowScope {\n if (input !== null && typeof input === 'object' && checkedAgainst(input, schema)) return input;\n const record = (value: unknown): value is Record<string, unknown> => !!value && typeof value === 'object' && !Array.isArray(value);\n /* A snapshot checked against another catalog — the same policy's constant scope under a second deployment of the\n App in this process, after a reload — is already plain frozen data this module made, so it is checked as it is\n and kept against this catalog too. Copied, its copy was a new object each time, which the caches keyed by a\n scope (tenantReadScope.ts) never found again: every request after a reload checked and composed afresh. */\n const scope = isRowScopeSnapshot(input) ? input : captureRowScope(input);\n const fixed = isFixedScopeCatalog(schema);\n const text = fixed && scope !== input ? rowScopeText(scope) : undefined;\n const held = text === undefined ? undefined : byText.get(schema);\n const known = text === undefined ? undefined : held?.get(text);\n if (known) return known;\n rowScopeCounters.checked++;\n let conditions = 0;\n for (const [id, policy] of Object.entries(scope)) {\n const table = findTable(schema, id);\n if (!table) return invalid();\n if (typeof policy === 'boolean') continue;\n if (\n !record(policy) ||\n Object.keys(policy).some((key) => key !== 'groups' && key !== 'binary') ||\n !Array.isArray(policy.groups) ||\n !policy.groups.length\n )\n return invalid();\n if (Object.hasOwn(policy, 'binary')) {\n if (\n schema.dialect !== 'sqlite' ||\n !Array.isArray(policy.binary) ||\n policy.binary.some((name) => typeof name !== 'string' || !table.columns.some((column) => column.name === name)) ||\n new Set(policy.binary).size !== policy.binary.length\n )\n return invalid();\n }\n for (const group of policy.groups) {\n if (!Array.isArray(group) || !group.length) return invalid();\n for (const condition of group) {\n if (++conditions > ROW_SCOPE_LIMITS.conditions || !record(condition)) return invalid();\n if (Object.keys(condition).some((key) => !['column', 'op', 'value'].includes(key))) return invalid();\n if (typeof condition.column !== 'string' || !table.columns.some((column) => column.name === condition.column)) return invalid();\n if (typeof condition.op !== 'string' || !OPS.has(condition.op)) return invalid();\n if (condition.op === 'isNull' || condition.op === 'isNotNull') {\n if (Object.hasOwn(condition, 'value')) return invalid();\n } else if (condition.op === 'in') {\n if (!Array.isArray(condition.value) || condition.value.some((value) => typeof value !== 'string' && typeof value !== 'number'))\n return invalid();\n } else if (['contains', 'startsWith', 'endsWith', 'like'].includes(condition.op)) {\n if (typeof condition.value !== 'string') return invalid();\n } else if (!['string', 'number', 'boolean'].includes(typeof condition.value)) return invalid();\n }\n }\n }\n // Kept only against a catalog nothing can change; checked against one that can, it is checked again next time.\n if (fixed) {\n const against = snapshots.get(scope);\n if (against) against.add(schema);\n else snapshots.set(scope, new WeakSet([schema]));\n if (text !== undefined) {\n let kept = held;\n if (!kept) byText.set(schema, (kept = new Map()));\n if (kept.size >= BY_TEXT_HELD) kept.delete(kept.keys().next().value!);\n kept.set(text, scope);\n }\n }\n return scope as RowScope;\n}\n\n/**\n * A scope with each group's repeated conditions said once. Composed scopes\n * repeat themselves — a tenant's ownership condition and a membership rule\n * that names the same column and value — and `\"tenant_id\" = $1 AND\n * \"tenant_id\" = $2` is the same rows as either half, while the planner\n * estimates it as two independent predicates and misjudges every join above\n * it. Only a condition identical in every field is dropped:\n * a conjunction of one condition with itself is that condition, so no row\n * the scope admits or refuses changes.\n */\nfunction withoutRepeats(policy: RowScopeFilter): Filter {\n return {\n groups: policy.groups.map((group) => {\n const seen = new Set<string>();\n return group.filter((condition) => {\n // Every field, in a fixed order: a collation or a column comparison is a different condition.\n const key = JSON.stringify(\n Object.keys(condition)\n .sort()\n .map((name) => [name, (condition as unknown as Record<string, unknown>)[name]]),\n );\n if (seen.has(key)) return false;\n seen.add(key);\n return true;\n });\n }),\n };\n}\n\n/* A catalog's table names, lower-cased: what a scoped relation's name must not\n shadow. The same for every statement compiled over a catalog nothing can\n change, so it is listed once for such a catalog, not once per statement. */\nconst reservedOf = new WeakMap<DatabaseSchema, readonly string[]>();\nfunction reservedNames(schema: DatabaseSchema): readonly string[] {\n const kept = reservedOf.get(schema);\n if (kept) return kept;\n const names = schema.tables.map((table) => table.name.toLowerCase());\n if (isFixedScopeCatalog(schema)) reservedOf.set(schema, names);\n return names;\n}\n\n/** Every occurrence of a resource reads the same filtered relation, including\n * LEFT JOIN targets and correlated aggregate children. CTE bindings precede\n * outer filters, independent of SQL SELECT/FROM expression order. Call finish\n * only after all relations have been discovered, then number outer bindings.\n */\nexport class ScopedRelations {\n private readonly scope: RowScope | undefined;\n private readonly names = new Map<string, string>();\n private readonly reserved: Set<string>;\n private readonly definitions: string[] = [];\n private readonly params: unknown[] = [];\n private finished = false;\n private ordinal = 0;\n\n constructor(\n private schema: DatabaseSchema,\n private style: SqlStyle,\n scope?: RowScope,\n ) {\n this.scope = scope === undefined ? undefined : snapshotRowScope(schema, scope);\n this.reserved = new Set(reservedNames(schema));\n }\n\n table = (id: string): string => {\n if (this.scope === undefined) return qualifiedTable(this.schema, id, this.style);\n const existing = this.names.get(id);\n if (existing) return this.style.quote(existing);\n if (this.finished) throw new Error('Row scope relations must be planned before outer parameters.');\n const table = findTable(this.schema, id);\n if (!table || !table.columns.length) return invalid();\n // '$' cannot occur in JoinPlan's sanitized aliases; catalog names still\n // need a case-insensitive collision check to avoid CTE self-shadowing.\n let name: string;\n do {\n name = `_tw_scope$${++this.ordinal}`;\n } while (this.reserved.has(name.toLowerCase()));\n this.reserved.add(name.toLowerCase());\n this.names.set(id, name);\n const policy = this.scope[id] ?? false;\n const where =\n typeof policy === 'boolean'\n ? { text: policy ? '' : ` WHERE ${this.style.never ?? '1 = 0'}`, params: [] }\n : buildWhere(\n withoutRepeats(policy),\n this.style,\n this.params.length + 1,\n (column) => this.style.quote(column) + (policy.binary?.includes(column) ? ' COLLATE BINARY' : ''),\n undefined,\n undefined,\n undefined,\n false,\n (column) => table.columns.find((c) => c.name === column)?.type,\n );\n const columns = table.columns.map((column) => this.style.quote(column.name)).join(', ');\n // The style's hint (cteHint) goes between AS and the body: Postgres inlines the CTE, so the\n // scope's predicate meets the read's own filter, sort and limit instead of a copied slice.\n this.definitions.push(\n `${this.style.quote(name)} AS${this.style.cteHint ?? ''} (SELECT ${columns} FROM ${qualifiedTable(this.schema, id, this.style)}${where.text})`,\n );\n this.params.push(...where.params);\n return this.style.quote(name);\n };\n\n private readonly narrowed = new Map<string, string>();\n\n /**\n * A table's rows narrowed by an authored filter over its own columns — the\n * rows a rollup counts (rollups.ts) — as one more definition here. A CTE\n * rather than a WHERE inside the correlated subquery, for the parameters:\n * a subquery can sit in the SELECT list, the WHERE and the ORDER BY at\n * once, and `?` placeholders bind by position, so values written there\n * would interleave with the outer filter's. Here they bind first, with the\n * scope's, and the subquery names the relation with no values of its own.\n * Read from the scoped relation where there is a scope, so the filter\n * narrows within the reader's rows, never beside them.\n */\n filtered = (id: string, filter: Filter, written?: Readonly<Record<string, (own: (column: string) => string) => string>>): string => {\n const key = `${id}\\u0000${JSON.stringify(filter)}\\u0000${Object.keys(written ?? {}).join(',')}`;\n const existing = this.narrowed.get(key);\n if (existing) return this.style.quote(existing);\n if (this.finished) throw new Error('Filtered relations must be planned before outer parameters.');\n const source = this.table(id);\n const table = findTable(this.schema, id);\n if (!table || !table.columns.length) return invalid();\n let name: string;\n do {\n name = `_tw_scope$${++this.ordinal}`;\n } while (this.reserved.has(name.toLowerCase()));\n this.reserved.add(name.toLowerCase());\n this.narrowed.set(key, name);\n const q = this.style.quote;\n /* A path (`sales_order_id.status`) is the column it lands on, read by one scalar subquery that walks\n its references from this row through the statement's relations — so a row policy on the order\n narrows what the path can see, as it does a list's join. Planned before the WHERE is written, for\n the parameters: a relation's own scope binds ahead of this filter's values. */\n const named = filter.groups.flatMap((group) => group.flatMap((c) => [c.column, ...(c.other === undefined ? [] : [c.other])]));\n const row = named.some((column) => column.includes('.')) ? '_tw_row' : undefined;\n const own = (column: string) => (row ? `${q(row)}.${q(column)}` : q(column));\n const paths = new Map<string, string>();\n for (const path of named) {\n // A computed column of the row (a rollup's query, rollups.ts `computed`): its expression over this row's columns.\n if (written && Object.hasOwn(written, path)) {\n if (!paths.has(path)) paths.set(path, `(${written[path](own)})`);\n continue;\n }\n if (!path.includes('.') || paths.has(path)) continue;\n const { resolved } = tryResolvePath(this.schema, id, path);\n if (!resolved || !resolved.hops.length || resolved.rollup || resolved.computed || resolved.tags || resolved.jsonKey) return invalid();\n const aliases = resolved.hops.map(() => `_tw_hop$${++this.ordinal}`);\n const joins = resolved.hops.map((hop, at) => {\n const on = hop.to.columns\n .map(\n (to, i) => `${q(aliases[at])}.${q(to)} = ${at ? `${q(aliases[at - 1])}.${q(hop.from.columns[i])}` : own(hop.from.columns[i])}`,\n )\n .join(' AND ');\n return { relation: `${this.table(hop.to.table)} ${q(aliases[at])}`, on };\n });\n const [first, ...rest] = joins;\n paths.set(\n path,\n `(SELECT ${q(aliases[aliases.length - 1])}.${q(resolved.column.name)} FROM ${first.relation}` +\n rest.map((join) => ` JOIN ${join.relation} ON ${join.on}`).join('') +\n ` WHERE ${first.on})`,\n );\n }\n const local = (column: string) => paths.get(column) ?? own(column);\n // A column's type, or a path's where it lands on a column: a computed one is an expression, compared as written.\n const typeOf = (column: string) => {\n if (written && Object.hasOwn(written, column)) return undefined;\n if (!column.includes('.')) return table.columns.find((c) => c.name === column)?.type;\n return plainType(this.schema, id, column);\n };\n const where = buildWhere(filter, this.style, this.params.length + 1, local, local, undefined, undefined, false, typeOf);\n const columns = table.columns.map((column) => q(column.name)).join(', ');\n this.definitions.push(`${q(name)} AS (SELECT ${columns} FROM ${source}${row ? ` ${q(row)}` : ''}${where.text})`);\n this.params.push(...where.params);\n return this.style.quote(name);\n };\n\n finish(): SqlFragment {\n this.finished = true;\n return { text: this.definitions.length ? `WITH ${this.definitions.join(',\\n ')}\\n` : '', params: [...this.params] };\n }\n}\n","/**\n * Multi-table views, built out of **reference paths**.\n *\n * The `Filter` in filter.ts addresses exactly one table and must keep doing\n * so: it is the shape every read goes through, and widening it to carry joins\n * would put join-building into every code path that filters anything. So the\n * multi-table story is a separate idea rather than a bigger filter, and the\n * idea is dot-walking:\n *\n * invoice customer_id.name contains Harbour\n * work_order assigned_to.last_name = Petrova\n * and customer_id.country_code.name = Australia\n *\n * A path is a chain of foreign keys starting at the base table, ending at a\n * column. `customer_id.name` means \"follow invoice.customer_id to customer,\n * then take name\". Nothing about it is guessed: the foreign key graph is\n * already introspected, so every hop is derivable, checkable, and reversible\n * into SQL — a path compiles to a LEFT JOIN chain and a qualified column.\n *\n * One mechanism answers three separate questions. Filtering across a long\n * walk, showing a field from a related table, and the composer UI are all the\n * same thing seen from three angles; the composer is a visual editor for the\n * path set.\n *\n * Two properties are load-bearing and are pinned by tests:\n *\n * - **LEFT JOIN, never INNER.** A null foreign key must leave the base row\n * in the result with nulls beside it. A composer that quietly drops rows\n * is worse than no composer at all, because the number it shows you is\n * wrong in a direction you cannot see.\n * - **Every value is a bound parameter.** Only identifiers are ever quoted\n * into the text, and they are quoted by the dialect's `SqlStyle`. The\n * WHERE clause is still built by `buildWhere`, which now takes an\n * identifier resolver so a path can become `\"customer_1\".\"name\"` without\n * a second predicate builder existing to disagree with the first.\n */\nimport {\n ANSI_STYLE,\n buildWhere,\n collate,\n orderClauseSql,\n type Condition,\n type Filter,\n type Op,\n type SqlFragment,\n type SqlStyle,\n} from './filter.js';\nimport { clampLimit, clampOffset, countStatement } from './reads.js';\nimport {\n aggregateInput,\n declaredOrder,\n findTable,\n type ColumnMeta,\n type DatabaseSchema,\n type ForeignKey,\n type TableMeta,\n} from './schema.js';\nimport { ANY_CONDITION, anyConditions, expandSegments, parseQuery, splitClauseTail, type QueryError, type QueryPerson } from './query.js';\nimport { stitchedListProblem } from './stitchedPaths.js';\nimport { qualifiedTable } from './sqlRelation.js';\nimport { ScopedRelations, type RowScope } from './rowScope.js';\nimport { slug } from './slug.js';\nimport { maskQuoted } from './maskQuoted.js';\nimport {\n type ViewColumn,\n type ViewCondition,\n type ViewFilter,\n type ViewOrder,\n type AggregateFn,\n type ViewAggregate,\n type ViewDef,\n PathError,\n type ResolvedPath,\n AGGREGATE_FNS,\n resolveAggregate,\n tryResolvePath,\n labelColumnMeta,\n backrefsTo,\n} from './viewPaths.js';\nimport { viewPaths, withViewComputed, withViewLabels } from './viewLabels.js';\nimport { rollupColumnMeta } from './rollups.js';\nimport { TAGS_COLUMN, tagsColumnMeta } from './tagLists.js';\nimport { computedColumnMeta } from './computed.js';\nimport { withViewReferences } from './declaredReferences.js';\nexport { withViewComputed, withViewLabels } from './viewLabels.js';\nexport {\n type ViewColumn,\n type ViewFilter,\n type ViewOrder,\n type AggregateFn,\n type ViewAggregate,\n type ViewDef,\n MAX_PATH_DEPTH,\n PathError,\n hopName,\n type Hop,\n hopsFrom,\n type ResolvedPath,\n resolvePath,\n backrefsTo,\n resolveAggregate,\n tableAtPrefix,\n tryResolvePath,\n} from './viewPaths.js';\n\n/* ---------- compilation ---------- */\n\n/* A view's filter as a column filter, for `buildWhere`. A condition's\n collation travels with it: dropped here, a keyset comparison would fall\n back to the column's collation while its ORDER BY kept the binary one. */\nfunction viewConditions(view: ViewDef): Filter | undefined {\n return view.filter?.groups.length\n ? {\n groups: view.filter.groups.map((group) =>\n group.map((c): Condition => ({\n column: c.path,\n op: c.op,\n value: c.value,\n ...(c.collation === undefined ? {} : { collation: c.collation }),\n ...(c.other === undefined ? {} : { other: c.other }),\n ...(c.any === undefined ? {} : { any: c.any }),\n })),\n ),\n }\n : undefined;\n}\n\nexport interface CompiledColumn {\n /** The path this column came from. */\n path: string;\n /** The output name in the result set. */\n name: string;\n /** Table the value actually lives on, for the UI's grouping. */\n table: string;\n column: string;\n type: string;\n nullable: boolean;\n primaryKey: boolean;\n /** How many references were followed to reach it. 0 is the base table. */\n hops: number;\n /** Set when the column is a summary of a many-valued relationship; `latest` is a rollup's newest row (rollups.ts). */\n aggregate?: AggregateFn | 'latest';\n}\n\nexport interface CompiledJoin {\n alias: string;\n table: string;\n /** The path prefix this join corresponds to, e.g. `customer_id.country_code`. */\n path: string;\n}\n\ninterface CompiledView {\n text: string;\n params: unknown[];\n columns: CompiledColumn[];\n joins: CompiledJoin[];\n limit: number;\n}\n\n/**\n * Alias generation.\n *\n * Two requirements pull in opposite directions. Aliases must never collide —\n * `work_order` in the demo has three separate references to `employee`, so a\n * scheme keyed on the target table alone produces three joins all called\n * `employee` and a statement that does not parse. And they must be stable, so\n * the same view compiles to the same SQL every time and a diff of the explain\n * panel means something.\n *\n * Both fall out of keying on the *path prefix* rather than the table: a prefix\n * identifies a join uniquely, so the same path used twice reuses one join and\n * two paths through the same table get two. The readable table name is kept as\n * a prefix and made unique by an ordinal assigned in first-use order.\n */\nexport class JoinPlan {\n readonly joins: CompiledJoin[] = [];\n private byPrefix = new Map<string, string>();\n private n = 0;\n\n constructor(\n private schema: DatabaseSchema,\n base: TableMeta,\n private style: SqlStyle,\n private relation: (id: string) => string = (id) => qualifiedTable(schema, id, style),\n ) {\n this.byPrefix.set('', alias(base.name, 0));\n }\n\n get baseAlias(): string {\n return this.byPrefix.get('')!;\n }\n\n /** The alias for a path prefix, creating the join chain if it is new. */\n aliasFor(hops: ForeignKey[], hopNames: string[]): string {\n let prefix = '';\n for (let i = 0; i < hops.length; i++) {\n prefix = prefix ? `${prefix}.${hopNames[i]}` : hopNames[i];\n /* Already planned: the same path used twice must reuse one join, not\n add a second copy of the same table under a different alias. */\n if (this.byPrefix.has(prefix)) continue;\n const target = findTable(this.schema, hops[i].to.table)!;\n this.byPrefix.set(prefix, alias(target.name, ++this.n));\n this.joins.push({ alias: this.byPrefix.get(prefix)!, table: target.id, path: prefix });\n }\n return this.byPrefix.get(prefix) ?? this.baseAlias;\n }\n\n /** The fully qualified, quoted SQL for a resolved path's final column. */\n qualify(resolved: ResolvedPath): string {\n const owner = this.aliasFor(resolved.hops, resolved.hopNames);\n return `${this.style.quote(owner)}.${this.style.quote(resolved.column.name)}`;\n }\n\n /** The relation a table is read as in this statement: its scoped CTE, or the table itself. */\n relationOf(id: string): string {\n return this.relation(id);\n }\n\n /** The alias of a path prefix that has already been planned, or the base. */\n aliasOfPrefix(prefix: string): string {\n return this.byPrefix.get(prefix) ?? this.baseAlias;\n }\n\n /**\n * A fresh alias for something that is not a join — a subquery's table.\n *\n * It draws from the same counter as the joins, which is what guarantees a\n * correlated subquery over `employee` cannot pick the alias a LEFT JOIN\n * over `employee` is already using. Two `employee_3`s in one statement is\n * a self-join nobody asked for, and the wrong answer rather than an error.\n */\n subAlias(name: string): string {\n return alias(name, ++this.n);\n }\n\n /**\n * The JOIN clauses, in the order the aliases were created.\n *\n * Order matters for more than tidiness: a join may only reference an alias\n * introduced before it, and creation order is exactly path order, so a\n * nested hop always follows the hop it hangs off.\n */\n clauses(): string[] {\n return this.joins.map((join) => {\n const parts = join.path.split('.');\n const parentPrefix = parts.slice(0, -1).join('.');\n const parent = this.byPrefix.get(parentPrefix)!;\n const fk = this.fkFor(join.path)!;\n const target = findTable(this.schema, join.table)!;\n /* Every column of the key, not just the first. A composite foreign key\n joined on one column is a cross product wearing a join's clothes. */\n const on = fk.to.columns\n .map(\n (toCol, i) =>\n `${this.style.quote(join.alias)}.${this.style.quote(toCol)} = ` +\n `${this.style.quote(parent)}.${this.style.quote(fk.from.columns[i] ?? fk.from.columns[0])}`,\n )\n .join(' AND ');\n /* LEFT, always. An INNER JOIN here would drop every base row whose\n foreign key is null — silently, and in a direction that looks like\n the data simply is not there. */\n return ` LEFT JOIN ${this.relation(target.id)} ${this.style.quote(join.alias)} ON ${on}`;\n });\n }\n\n private fkChain = new Map<string, ForeignKey>();\n\n /** Remember which key produced each join, so `clauses` can rebuild the ON. */\n record(path: string, fk: ForeignKey): void {\n if (!this.fkChain.has(path)) this.fkChain.set(path, fk);\n }\n\n private fkFor(path: string): ForeignKey | undefined {\n return this.fkChain.get(path);\n }\n}\n\n/** Alias text: the table's own name, made unique by an ordinal. */\nfunction alias(name: string, n: number): string {\n /* Truncated because Postgres silently cuts identifiers at 63 bytes, and a\n silent cut is exactly how two long table names become one alias. */\n const safe = name.replace(/[^A-Za-z0-9_]/g, '_').slice(0, 40) || 't';\n return `${safe}_${n}`;\n}\n\n/**\n * Compile a view to one parameterised SELECT.\n *\n * Order of work is deliberate and is what makes the output deterministic:\n * columns, then the filter, then the sort. Aliases are handed out on first\n * use, so the same view always produces the same statement.\n */\nexport function compileView(\n schema: DatabaseSchema,\n view: ViewDef,\n style: SqlStyle,\n offset: number | unknown = 0,\n scope?: RowScope,\n): CompiledView {\n // The App's soft keys, where this catalog is the database's own (declaredReferences.ts).\n schema = withViewReferences(schema, view.references);\n const base = findTable(schema, view.base);\n if (!base) {\n throw new PathError(`No table called \"${view.base}\" in this database, so the view has nothing to start from.`, view.base, view.base, 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\n const { resolve, qualify, membership, exists, typeOf } = viewPaths(schema, view, plan, style, relations);\n\n /* An empty column list means the base table's own columns. Expanding it\n here rather than emitting `SELECT *` keeps the result's column list\n knowable without running the statement, which is what the composer's\n field checklist reads. */\n const wanted: ViewColumn[] = view.columns?.length ? view.columns : base.columns.map((c) => ({ path: c.name }));\n\n const seen = new Map<string, number>();\n const columns: CompiledColumn[] = [];\n const select: string[] = [];\n\n for (const col of wanted) {\n const resolved = resolve(col.path);\n const sql = qualify(col.path);\n /* Two paths can legitimately end on the same name — `customer_id.name`\n and `sold_by.name` are both \"name\" if aliased carelessly. The path is\n already unique, so it is the default; an explicit collision gets a\n numeric suffix rather than one column quietly overwriting the other. */\n let name = col.alias?.trim() || resolved.path;\n const count = seen.get(name) ?? 0;\n seen.set(name, count + 1);\n if (count) name = `${name}_${count + 1}`;\n\n columns.push({\n path: resolved.path,\n name,\n table: resolved.table.id,\n column: resolved.column.name,\n type: resolved.column.type,\n /* A column on a joined table is nullable in the result even when the\n column itself is NOT NULL: the LEFT JOIN can produce no row at all.\n Saying otherwise would have the UI render a real null as an error. */\n nullable: resolved.column.nullable || resolved.hops.length > 0,\n primaryKey: resolved.column.primaryKey,\n hops: resolved.hops.length,\n // A rollup reads like a column and is a summary: nothing edits it.\n ...(resolved.rollup && !resolved.rollup.through ? { aggregate: resolved.rollup.fn } : {}),\n });\n select.push(`${sql} AS ${style.quote(name)}`);\n }\n\n /* Aggregates come after the plain columns, and after them on purpose: a\n summary hanging off a joined table needs that join's alias to exist, and\n resolving columns first is what puts it there. */\n for (const aggregate of view.aggregates ?? []) {\n const resolved = resolveAggregate(schema, view.base, aggregate);\n /* The parent side is a path prefix, so an aggregate can hang off a joined\n table as easily as off the base — \"invoice lines on the invoice this\n shipment belongs to\" is the same construction one hop along. */\n if (resolved.on) {\n /* Qualifying any column on the parent is what plans the joins leading\n to it and records their keys. Doing it through the same `qualify`\n the columns use is deliberate: two ways of planning a join is two\n ways for the aliases to disagree. */\n qualify(`${resolved.on}.${resolved.parent.columns[0].name}`);\n }\n const parentAlias = resolved.on ? plan.aliasOfPrefix(resolved.on) : plan.baseAlias;\n\n const childAlias = plan.subAlias(resolved.child.name);\n const on = resolved.fk.to.columns\n .map(\n (toCol, i) =>\n `${style.quote(childAlias)}.${style.quote(resolved.fk.from.columns[i] ?? resolved.fk.from.columns[0])} = ` +\n `${style.quote(parentAlias)}.${style.quote(toCol)}`,\n )\n .join(' AND ');\n\n /* COUNT(*) rather than COUNT(column): a column count skips nulls, which\n turns \"how many parts\" into \"how many parts have a quantity\" without\n saying so. SUM over no rows is left as NULL rather than coerced to 0 —\n a work order with no parts has no total, and 0 is a claim about money\n that nobody made. */\n const expression =\n resolved.aggregate.fn === 'count'\n ? 'COUNT(*)'\n : `${resolved.aggregate.fn.toUpperCase()}(${aggregateInput(\n `${style.quote(childAlias)}.${style.quote(resolved.column!.name)}`,\n resolved.column!.type,\n style.dialect,\n )})`;\n\n let name = resolved.name;\n const count = seen.get(name) ?? 0;\n seen.set(name, count + 1);\n if (count) name = `${name}_${count + 1}`;\n\n select.push(\n `(SELECT ${expression} FROM ${relations.table(resolved.child.id)} ${style.quote(childAlias)}` +\n ` WHERE ${on}) AS ${style.quote(name)}`,\n );\n columns.push({\n path: resolved.name,\n name,\n table: resolved.child.id,\n column: resolved.column?.name ?? '*',\n type: resolved.aggregate.fn === 'count' ? 'integer' : (resolved.column?.type ?? 'integer'),\n // COUNT is never null; every other summary is, over an empty child set.\n nullable: resolved.aggregate.fn !== 'count',\n primaryKey: false,\n hops: resolved.on ? resolved.on.split('.').length : 0,\n aggregate: resolved.aggregate.fn,\n });\n }\n\n /* The filter is compiled by `buildWhere`, not beside it. One predicate\n builder means one place where a value could stop being a parameter. */\n const conditions = viewConditions(view);\n // Preserve first-use alias ordering, but discover filter and sort joins\n // before freezing the scope prefix and numbering ordinary parameters.\n for (const group of conditions?.groups ?? []) {\n for (const condition of group) {\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 const aliasNames = new Set((view.aggregates ?? []).map((a) => a.alias?.trim()).filter(Boolean));\n const order = (view.orderBy ?? []).flatMap((o) => {\n if (o.nulls === undefined) return [orderTerm(o)];\n if (o.nulls !== 'last' || o.alias || o.near !== undefined) {\n throw new PathError(`\"${o.path}\" puts nulls last only when it is a column.`, o.path, o.path, 0);\n }\n /* NULLS LAST in both directions, said the one way every dialect reads:\n a null sorts after every value whichever way the column runs. */\n return [`${qualify(o.path)} IS NULL ASC`, orderTerm(o)];\n });\n function orderTerm(o: ViewOrder) {\n if (o.collation !== undefined && (o.alias || o.near !== undefined)) {\n throw new PathError(`\"${o.path}\" orders by a column's own text only when it is a column.`, o.path, o.path, 0);\n }\n if (o.alias) {\n /* Refused by name rather than left to the engine: SQLite quietly\n treats an unknown ORDER BY identifier as a new error anyway, but\n \"no such column: invoicez\" would not say the fix is the alias two\n words back. */\n if (!aliasNames.has(o.path)) {\n throw new PathError(`\"${o.path}\" is not an aggregate alias on this view.`, o.path, o.path, 0);\n }\n return `${style.quote(o.path)} ${o.direction === 'desc' ? 'DESC' : 'ASC'}`;\n }\n if (o.near !== undefined) {\n /* Closest first, as a literal rather than a parameter: the ORDER BY is\n written before the WHERE's parameters are numbered, and a finite,\n safe number carries nothing but itself. Parenthesized, so a negative\n value never meets the minus as \"--\", a comment in SQL. */\n if (typeof o.near !== 'number' || !Number.isFinite(o.near) || Math.abs(o.near) > Number.MAX_SAFE_INTEGER) {\n throw new PathError(`\"near\" on \"${o.path}\" must be a finite number.`, o.path, o.path, 0);\n }\n return `ABS(${qualify(o.path)} - (${String(o.near)})) ${o.direction === 'desc' ? 'DESC' : 'ASC'}`;\n }\n /* A vocabulary sorts as declared (schema.ts declaredOrder); an order by\n bytes asked for the bytes, for a cursor that compares them. */\n if (o.collation === undefined)\n return { expression: qualify(o.path), direction: o.direction, declared: declaredOrder(resolve(o.path).column) };\n return `${collate(qualify(o.path), o.collation, style)} ${o.direction === 'desc' ? 'DESC' : 'ASC'}`;\n }\n\n const limit = clampLimit(view.limit);\n const skip = clampOffset(offset);\n const joins = plan.clauses();\n const prefix = relations.finish();\n /* The other side of a column comparison is a path like any other: resolved\n against this catalog, so a reader's pruned graph refuses one it may not\n read exactly as it refuses the left. */\n const where = buildWhere(conditions, style, prefix.params.length + 1, qualify, qualify, membership, exists, false, typeOf);\n\n /* Placeholder numbering continues across the whole statement, which is the\n one thing Postgres needs and SQLite does not — and the exact place a join\n builder gets it wrong, because the joins sit between the SELECT list and\n the WHERE clause and are easy to count as if they carried parameters. */\n const sorted = orderClauseSql(order, style, prefix.params.length + where.params.length + 1);\n const n = prefix.params.length + where.params.length + sorted.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.replace(/^ WHERE /, ' WHERE '));\n if (sorted.text) lines.push(sorted.text);\n lines.push(` LIMIT ${style.placeholder(n + 1)} OFFSET ${style.placeholder(n + 2)}`);\n\n return {\n text: prefix.text + lines.join('\\n'),\n params: [...prefix.params, ...where.params, ...sorted.params, limit, skip],\n columns,\n joins: plan.joins,\n limit,\n };\n}\n\n/* ---------- the query language, over paths ----------\n\n The composer's filter box takes the same language as the bar at the top,\n because two languages for one idea is one too many. It is parsed by the\n same parser, too: `parseQuery` already handles and/or, parentheses, quoting,\n relative dates and per-column value coercion, and a second implementation\n of those would be a second set of bugs.\n\n The trick that makes it work is a *synthetic* table. `parseQuery` resolves\n every field against a table's column list, so the base table is handed to it\n with one extra column per path mentioned in the text, each carrying the\n declared type of the column the path actually ends on. Coercion, date\n handling and \"did you mean\" then work over paths for free.\n\n Paths are resolved on demand rather than enumerated: `work_order.parent_id`\n points back at `work_order`, so the set of paths four hops deep is tens of\n thousands of entries and building it per keystroke would be absurd. */\n\n/* One segment of a path: a column name, or — for a composite key — the\n columns of it joined with `+`, exactly as `hopName` writes them.\n\n `+` has to be admitted here as well as in `parseQuery`'s FIELD, or the two\n disagree about where a token begins: the scanner reads only `line_no.sku`\n out of `invoice_id+line_no.sku`, builds the synthetic column for a path that\n does not exist, and a composite hop becomes something you can show but never\n filter. It is legal only *between* identifier characters, never leading —\n a leading `+` would let a signed number start a path. */\nconst SEGMENT = String.raw`[A-Za-z_$][\\w$]*(?:\\+[A-Za-z_$][\\w$]*)*`;\n\n/* A dotted token. `@` and `+` are excluded from the lookbehind on purpose:\n without `@`, `email = ops@harbour.example` reads its own value as a path and\n reports a confident error about a table that was never mentioned; without\n `+`, the tail of a composite hop matches on its own and reports the same\n kind of confident error about half a key. */\nconst DOTTED_TOKEN = new RegExp(String.raw`(?<![\\w.$@+])(${SEGMENT}(?:\\.${SEGMENT})+)`, 'g');\n\n/** What makes a token a *field* rather than a value: an operator after it. */\nconst OPERATOR_AFTER = /^\\s*(?:!=|<=|>=|=|<|>|\\bcontains\\b|\\bstartswith\\b|\\bendswith\\b|\\blike\\b|\\bnot\\s+in\\b|\\bin\\b|\\bis\\b)/i;\n\n/** …or a word before it that can only be followed by a field. */\nconst FIELD_BEFORE = /(?:^|[\\s(,])(?:and|or|where|filter|show|select|sort|by)\\s*$|(?:^|,)\\s*$/i;\n\nexport interface ParsedView {\n filter: ViewFilter;\n columns: string[];\n orderBy: ViewOrder[];\n limit?: number;\n errors: QueryError[];\n}\n\n/**\n * Parse the composer's filter/clause text against a base table.\n *\n * Returns errors rather than throwing, because this runs on every keystroke\n * and a half-typed path is the normal state of the input, not an exception.\n */\nexport function parseViewQuery(\n schema: DatabaseSchema,\n baseId: string,\n text: string,\n now?: Date,\n options: {\n aggregateAliases?: string[];\n /** Who `me` is for this request; null, nobody signed in; absent, `me` is refused (query.ts). */\n me?: QueryPerson | null;\n /** Admit a column on a condition's right-hand side (the default); an API's standing query does not. */\n columnValues?: boolean;\n } = {},\n): ParsedView {\n const empty: ParsedView = { filter: { groups: [] }, columns: [], orderBy: [], errors: [] };\n const base = findTable(schema, baseId);\n if (!base) {\n return { ...empty, errors: [{ message: `No table called \"${baseId}\" in this database.` }] };\n }\n const written = String(text ?? '').trim();\n if (!written) return empty;\n\n const errors: QueryError[] = [];\n /* A segment's name is written out first (query.ts expandSegments), so the paths, rollups and computed\n columns its conditions name are found below as if they had been typed; a position is said in the text\n as written. */\n let body = written;\n let said = (at: number) => at;\n if (base.segments) {\n const { head } = splitClauseTail(written);\n const start = head ? written.indexOf(head) : -1;\n if (start >= 0) {\n const expanded = expandSegments(head, base.segments);\n if (expanded.error) return { ...empty, errors: [{ message: expanded.error.message, at: start + (expanded.error.at ?? 0) }] };\n if (expanded.text !== head) {\n body = written.slice(0, start) + expanded.text + written.slice(start + head.length);\n const end = start + expanded.text.length;\n said = (at) => (at < start ? at : at < end ? start + expanded.at(at - start) : at - (expanded.text.length - head.length));\n }\n }\n }\n const extra: ColumnMeta[] = [];\n /* Each `any`/`no` condition is read on its own, against the child, and stands in the text the parser reads as a\n synthetic column asked `is (not) empty`; it becomes the condition it is when the parse comes back. */\n const anys = new Map<string, ViewCondition>();\n if (/\\b(?:any|no)\\s/i.test(body)) {\n const read = anyRowsOf(schema, base, body, now, options, anys, extra);\n if (read.errors.length)\n return { ...empty, errors: read.errors.map((error) => ({ ...error, at: error.at === undefined ? undefined : said(error.at) })) };\n if (read.text !== body) {\n const outer = said;\n body = read.text;\n said = (at) => outer(read.at(at));\n }\n }\n const added = new Set<string>();\n /* Paths already reported as broken. The synthetic table cannot contain\n them, so the parser will reach them too and say \"not a column on invoice\"\n — true, useless, and directly under a message that named the real\n problem. Two errors for one mistake reads as two mistakes. */\n const failed = new Set<string>();\n\n const scanned = maskQuoted(body);\n for (const match of scanned.matchAll(DOTTED_TOKEN)) {\n const path = match[1];\n const start = match.index ?? 0;\n /* Whether this token is being used as a field decides whether a failure\n to resolve is worth reporting. In value position — `external_ref =\n a.b` — a dotted word that is not a path is simply a value, and an error\n about it would be noise on a query that is perfectly correct. */\n /* …or a path on the right of a comparison whose first hop is a column\n of the base: `on_hand < product_id.reorder_level` compares with it,\n so a hop that goes nowhere is a mistake to name, not text. */\n const isField =\n OPERATOR_AFTER.test(scanned.slice(start + path.length)) ||\n FIELD_BEFORE.test(scanned.slice(0, start)) ||\n (/(?:!=|<=|>=|=|<|>)\\s*$/.test(scanned.slice(0, start)) && base.columns.some((c) => c.name === path.split('.')[0].split('+')[0]));\n if (added.has(path)) continue;\n added.add(path);\n const { resolved, error } = tryResolvePath(schema, baseId, path);\n if (error) {\n /* Located at the token so the composer can underline the right word.\n The offset is into the filter text, and the caller shifts it. */\n if (isField) {\n errors.push({ message: error.message, at: said(start) });\n failed.add(path);\n }\n continue;\n }\n if (!resolved) continue;\n /* The synthetic column keeps the *declared* type of the real column, so\n `signed_up = last 30 days` through a path behaves exactly as it does\n on the base table. Nullability is widened because the join can miss. */\n extra.push({\n name: resolved.path,\n type: resolved.column.type,\n nullable: true,\n primaryKey: false,\n references: resolved.column.references,\n });\n }\n\n /* The base's own record label (`show $label`, `$label contains 12`) is a\n column of the synthetic table where the catalog serves a template. */\n if (base.labelTemplate && /(?<![\\w.$])\\$label(?![\\w$])/.test(scanned)) {\n extra.push(labelColumnMeta());\n }\n\n /* The base's rollups (rollups.ts) are columns of the synthetic table where\n the text names one, typed as the catalog serves them, so `sort\n line_total desc` and `open_deals > 0` parse and coerce like any number.\n A rollup through a reference is a dotted path, found above. */\n for (const [name, rollup] of Object.entries(base.rollups ?? {})) {\n if (added.has(name) || base.columns.some((c) => c.name === name)) continue;\n if (new RegExp(`(?<![\\\\w.$])${name}(?![\\\\w$])`).test(scanned)) extra.push(rollupColumnMeta(name, rollup));\n }\n /* And its computed columns (computed.ts), typed as they come out: `is_overdue = true`, `sort days_open desc`. */\n for (const [name, computed] of Object.entries(base.computed ?? {})) {\n if (added.has(name) || base.columns.some((c) => c.name === name)) continue;\n if (new RegExp(`(?<![\\\\w.$])${name}(?![\\\\w$])`).test(scanned)) extra.push(computedColumnMeta(name, computed));\n }\n /* And its tags (tagLists.ts), a column only `has` may ask: `tags has bug`, `show name, tags`. */\n if (\n base.tags &&\n !added.has(TAGS_COLUMN) &&\n !base.columns.some((c) => c.name === TAGS_COLUMN) &&\n new RegExp(`(?<![\\\\w.$])${TAGS_COLUMN}(?![\\\\w$])`).test(scanned)\n )\n extra.push(tagsColumnMeta());\n\n /* Aggregate aliases join the synthetic table so `sort invoices desc`\n parses — and only sort. A WHERE runs before the SELECT list exists, so\n filtering by an alias is refused below with the reason, not with\n \"not a column\", which is true and useless. */\n const aliases = new Set(options.aggregateAliases ?? []);\n for (const alias of aliases) {\n if (!added.has(alias)) extra.push({ name: alias, type: 'NUMERIC', nullable: true, primaryKey: false });\n }\n\n const synthetic: DatabaseSchema = {\n ...schema,\n tables: schema.tables.map((t) => (t.id === base.id ? { ...t, columns: [...t.columns, ...extra] } : t)),\n };\n\n /* The parser wants `<table> <clauses>`, which is also what the composer's\n hint text shows, so the base name is prepended rather than the grammar\n being special-cased. */\n const offset = base.id.length + 1;\n const parsed = parseQuery(`${base.id} ${body}`, {\n schema: synthetic,\n now,\n columnValues: options.columnValues !== false,\n ...(options.me !== undefined ? { me: options.me } : {}),\n });\n for (const err of parsed.errors) {\n if ([...failed].some((path) => err.message.includes(`\"${path}\"`))) continue;\n errors.push({ message: err.message, at: err.at === undefined ? undefined : said(Math.max(0, err.at - offset)) });\n }\n\n const query = parsed.query;\n if (!query) return { ...empty, errors };\n\n for (const group of query.filter.groups) {\n for (const c of group) {\n if (aliases.has(c.column)) {\n errors.push({\n message: `\"${c.column}\" is an aggregate result, and a filter runs before it exists. ` + 'Filter on the child table instead.',\n });\n }\n }\n }\n for (const column of query.columns ?? []) {\n if (aliases.has(column)) {\n errors.push({ message: `\"${column}\" is already shown — the aggregate adds its own column.` });\n }\n }\n\n return {\n filter: {\n groups: query.filter.groups.map((group) =>\n group.map(\n (c): ViewCondition =>\n anys.get(c.column) ??\n (c.other === undefined ? { path: c.column, op: c.op, value: c.value } : { path: c.column, op: c.op, other: c.other }),\n ),\n ),\n },\n columns: query.columns,\n orderBy: query.orderBy.map((o) =>\n aliases.has(o.column) ? { path: o.column, direction: o.direction, alias: true } : { path: o.column, direction: o.direction },\n ),\n limit: query.limit,\n errors,\n };\n}\n\n/* The name an `any` condition stands under while the parser reads the rest: no column can be called it. */\nconst ANY_STANDIN = '$any';\n\n/**\n * The `any`/`no` conditions of a view's text, each read against its child\n * table: the text with each written as a stand-in column asked `is (not)\n * empty`, the stand-ins (in `extra`) and the conditions they become (in\n * `anys`), and a position in the new text said in the old.\n *\n * The child is the table whose one reference points at the base, or the one\n * `via` names; its conditions are its own columns and paths up from it, and\n * its segments — what a row scope's narrowing reads (rowScope.ts `filtered`),\n * which is how the EXISTS is written. A second `any` inside, a rollup, a\n * computed column, tags or a record label are refused by name rather than\n * left to fail as SQL.\n */\nfunction anyRowsOf(\n schema: DatabaseSchema,\n base: TableMeta,\n body: string,\n now: Date | undefined,\n options: { me?: QueryPerson | null; columnValues?: boolean },\n anys: Map<string, ViewCondition>,\n extra: ColumnMeta[],\n): { text: string; at: (position: number) => number; errors: QueryError[] } {\n const same = { text: body, at: (position: number) => position, errors: [] as QueryError[] };\n const { head } = splitClauseTail(body);\n const start = head ? body.indexOf(head) : -1;\n if (start < 0) return same;\n const found = anyConditions(head);\n if (!found.length) return same;\n const errors: QueryError[] = [];\n const splices: Array<{ at: number; was: number; from: number; length: number }> = [];\n let text = '';\n let last = 0;\n for (const condition of found) {\n const at = start + condition.at;\n const [, word, childName, via, rest] = ANY_CONDITION.exec(condition.text)!;\n const fail = (message: string, offset = 0) => errors.push({ message, at: at + offset });\n const named = backrefsTo(schema, base.id).filter((b) => {\n const table = findTable(schema, b.child);\n return table && (table.id === childName || table.name === childName);\n });\n const through = via === undefined ? named : named.filter((b) => b.fk.from.columns.join('+') === via);\n if (!named.length) {\n fail(\n findTable(schema, childName) || schema.tables.some((t) => t.name === childName)\n ? `${childName} has no reference to ${base.name}, so \"${word} ${childName}\" has no rows to ask about.`\n : `No table called \"${childName}\" here.`,\n word.length + 1,\n );\n continue;\n }\n if (!through.length) {\n fail(\n `${childName} points at ${base.name} through ${named.map((b) => b.fk.from.columns.join('+')).join(', ')}, not \"${via}\".`,\n condition.text.indexOf(via!, word.length + childName.length + 1),\n );\n continue;\n }\n if (through.length > 1) {\n const columns = through.map((b) => b.fk.from.columns.join('+'));\n fail(\n `${childName} points at ${base.name} through ${columns.join(' and ')}: say which — ${word} ${childName} via ${columns[0]}.`,\n word.length + 1,\n );\n continue;\n }\n const { fk } = through[0];\n if (fk.stitched) {\n fail(\n `${childName} is in another source, on another connection, so a list of ${base.name} cannot ask about its rows: ` +\n 'each database filters what it holds.',\n word.length + 1,\n );\n continue;\n }\n const child = findTable(schema, fk.from.table)!;\n let filter: Filter | undefined;\n if (rest !== undefined) {\n const restAt = condition.text.length - rest.length;\n const parsed = parseViewQuery(schema, child.id, rest, now, { me: options.me, columnValues: options.columnValues });\n for (const error of parsed.errors) fail(error.message, restAt + (error.at ?? 0));\n if (parsed.errors.length) continue;\n if (parsed.columns.length || parsed.orderBy.length || parsed.limit !== undefined) {\n fail(`\"${word} ${childName}\" takes conditions only: a show, sort or limit there has no rows to shape.`, restAt);\n continue;\n }\n const unread = parsed.filter.groups\n .flat()\n .flatMap((c) => (c.any ? [c.path] : [c.path, ...(c.other === undefined ? [] : [c.other])]))\n .find((path) => {\n if (path.startsWith('any ') || path.startsWith('no ')) return true;\n const { resolved } = tryResolvePath(schema, child.id, path);\n return !resolved || Boolean(resolved.rollup || resolved.computed || resolved.tags || resolved.jsonKey || path.includes('$'));\n });\n if (unread !== undefined) {\n fail(\n /^(?:any|no)\\s/i.test(unread)\n ? `\"${word} ${childName}\" cannot ask about rows of its own rows: say that as its own condition.`\n : `\"${unread}\" is not a stored column of ${child.name}, and \"${word} ${childName}\" reads only those.`,\n restAt + Math.max(0, rest.indexOf(unread)),\n );\n continue;\n }\n const stitched = stitchedListProblem(schema, child.id, { filter: parsed.filter });\n if (stitched) {\n fail(stitched, restAt);\n continue;\n }\n filter = {\n groups: parsed.filter.groups.map((group) =>\n group.map((c): Condition =>\n c.other === undefined ? { column: c.path, op: c.op, value: c.value } : { column: c.path, op: c.op, other: c.other },\n ),\n ),\n };\n }\n const standin = `${ANY_STANDIN}${anys.size}`;\n const said = condition.text.replace(/\\s+/g, ' ');\n anys.set(standin, {\n path: said,\n op: word.toLowerCase() === 'no' ? 'isNull' : 'isNotNull',\n any: { via: `${child.id}.${fk.from.columns.join('+')}`, ...(filter ? { filter } : {}) },\n });\n extra.push({ name: standin, type: 'INTEGER', nullable: true, primaryKey: false });\n const written = `${standin} ${word.toLowerCase() === 'no' ? 'is empty' : 'is not empty'}`;\n text += body.slice(last, at);\n splices.push({ at, was: condition.text.length, from: text.length, length: written.length });\n text += written;\n last = at + condition.text.length;\n }\n if (errors.length) return { ...same, errors };\n text += body.slice(last);\n return {\n text,\n errors,\n at: (position) => {\n let shift = 0;\n for (const splice of splices) {\n if (position < splice.from) break;\n if (position < splice.from + splice.length) return splice.at;\n shift = splice.from + splice.length - (splice.at + splice.was);\n }\n return position - shift;\n },\n };\n}\n\n/**\n * How many rows the view matches, ignoring the page.\n *\n * A separate compilation rather than a wrapper around the other one, because\n * the two differ in more than a keyword: the SELECT list, the ORDER BY and the\n * page are all irrelevant here, and every aggregate in the view would be a\n * correlated subquery evaluated per row for a number nobody reads.\n *\n * The count is exact rather than estimated, and it is *base rows* — which is\n * only true because every join is to-one. That is the same property the whole\n * design rests on: a view never multiplies, so counting it is counting the\n * table it started from. With `limit`, it stops after that many.\n */\nexport function compileViewCount(schema: DatabaseSchema, view: ViewDef, style: SqlStyle, scope?: RowScope, limit?: number): SqlFragment {\n schema = withViewReferences(schema, view.references);\n const base = findTable(schema, view.base);\n if (!base) {\n throw new PathError(`No table called \"${view.base}\" in this database, so the view has nothing to start from.`, view.base, view.base, 0);\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 const { qualify, membership, exists, typeOf } = viewPaths(schema, view, plan, style, relations);\n\n const conditions = viewConditions(view);\n for (const group of conditions?.groups ?? []) {\n for (const condition of group) {\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 const joins = plan.clauses();\n const prefix = relations.finish();\n const where = buildWhere(conditions, style, prefix.params.length + 1, qualify, qualify, membership, exists, false, typeOf);\n\n /* Only filter joins affect row count. Projection-only joins and scalar\n aggregates deliberately do not register relations in this statement. */\n const relation = `${baseRelation} ${style.quote(plan.baseAlias)}` + (joins.length ? `\\n${joins.join('\\n')}` : '') + where.text;\n /* A bound stops the count the way every bounded count stops\n (countStatement): at most `limit` base rows are read. */\n const text =\n limit === undefined ? prefix.text + `SELECT COUNT(*) AS n\\n FROM ${relation}` : prefix.text + countStatement(relation, limit);\n return { text, params: [...prefix.params, ...where.params] };\n}\n\n/**\n * Render a view as the SQL it will become, values inline.\n *\n * The mirror of `explain` in query.ts and used the same way: this string never\n * reaches a database, and showing `\"customer_1\".\"name\" LIKE '%Harbour%'` next\n * to the path that produced it is how someone learns what the composer is\n * doing on their behalf.\n */\nexport function explainView(schema: DatabaseSchema, view: ViewDef, style: SqlStyle): string {\n const compiled = compileView(schema, withViewComputed(schema, withViewLabels(schema, view)), style);\n let i = 0;\n return compiled.text.replace(/\\$\\d+|\\?/g, () => sqlLiteral(compiled.params[i++]));\n}\n\n/**\n * One parameter, written the way SQL would have to spell it.\n *\n * Exported because a breakdown explains itself the same way a view does, and\n * two spellings of `NULL` — or two different ideas about escaping a quote —\n * would show up as a difference between two panes that claim to be the same\n * statement.\n */\nexport function sqlLiteral(value: unknown): string {\n if (value === null || value === undefined) return 'NULL';\n if (typeof value === 'number' || typeof value === 'boolean') return String(value);\n return `'${String(value).replace(/'/g, \"''\")}'`;\n}\n\n/* ---------- config file support ----------\n\n Views can be predefined in `tablewalk.json` beside `connections`, and are\n validated the same strict way: a malformed entry is an error naming the\n entry, never a silent skip. Quietly dropping a view someone wrote is the\n worst outcome — they see a shorter list and no reason for it. */\n\nexport function parseViewsConfig(raw: unknown, where: string): ViewDef[] {\n const views = (raw as { views?: unknown })?.views;\n if (views === undefined) return [];\n if (!Array.isArray(views)) throw new Error(`${where}: \"views\" must be an array.`);\n\n const seen = new Set<string>();\n return views.map((entry, i) => {\n const at = `${where}: view ${i + 1}`;\n if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {\n throw new Error(`${at} is not an object.`);\n }\n const v = entry as Record<string, unknown>;\n if (typeof v.name !== 'string' || !v.name.trim()) throw new Error(`${at} has no \"name\".`);\n\n /* A view may be written down instead of built out of objects. The text\n form needs a schema to resolve, which a config file does not have, so\n it is carried through and expanded when a connection is open — and any\n error it holds is reported against that view rather than swallowed. */\n const text = v.text === undefined ? undefined : String(v.text).trim();\n if (v.text !== undefined && (typeof v.text !== 'string' || !text)) {\n throw new Error(`${at} (\"${v.name}\") has a \"text\" that is not a non-empty string.`);\n }\n if (text && v.base !== undefined) {\n throw new Error(`${at} (\"${v.name}\") has both \"text\" and \"base\". The text form already names its table.`);\n }\n if (!text && (typeof v.base !== 'string' || !v.base.trim())) {\n throw new Error(`${at} (\"${v.name}\") has no \"base\" table.`);\n }\n const id = v.id === undefined ? `config-${slug(v.name, 'view')}` : String(v.id);\n if (v.id !== undefined && (typeof v.id !== 'string' || !v.id.trim())) {\n throw new Error(`${at} (\"${v.name}\") has an \"id\" that is not a non-empty string.`);\n }\n if (seen.has(id)) throw new Error(`${at} (\"${v.name}\") repeats the id \"${id}\".`);\n seen.add(id);\n\n const columns = parseColumns(v.columns, at, String(v.name));\n const aggregates = parseAggregates(v.aggregates, at, String(v.name));\n const filter = parseFilter(v.filter, at, String(v.name));\n const orderBy = parseOrder(v.orderBy, at, String(v.name));\n\n if (v.limit !== undefined && (typeof v.limit !== 'number' || !Number.isFinite(v.limit))) {\n throw new Error(`${at} (\"${v.name}\") has a \"limit\" that is not a number.`);\n }\n if (v.connection !== undefined && typeof v.connection !== 'string') {\n throw new Error(`${at} (\"${v.name}\") has a \"connection\" that is not a string.`);\n }\n\n return {\n id,\n name: v.name.trim(),\n base: text ? text.trim().split(/\\s+/)[0] : (v.base as string).trim(),\n text,\n columns,\n aggregates,\n filter,\n orderBy,\n limit: v.limit as number | undefined,\n connection: v.connection as string | undefined,\n source: 'config' as const,\n };\n });\n}\n\nfunction parseColumns(raw: unknown, at: string, name: string): ViewColumn[] {\n if (raw === undefined) return [];\n if (!Array.isArray(raw)) throw new Error(`${at} (\"${name}\") has \"columns\" that is not an array.`);\n return raw.map((col, j) => {\n const where = `${at} (\"${name}\"), column ${j + 1}`;\n if (typeof col === 'string') {\n if (!col.trim()) throw new Error(`${where} is empty.`);\n return { path: col.trim() };\n }\n if (!col || typeof col !== 'object') throw new Error(`${where} is not a string or an object.`);\n const c = col as Record<string, unknown>;\n if (typeof c.path !== 'string' || !c.path.trim()) throw new Error(`${where} has no \"path\".`);\n if (c.alias !== undefined && (typeof c.alias !== 'string' || !c.alias.trim())) {\n throw new Error(`${where} has an \"alias\" that is not a non-empty string.`);\n }\n return { path: c.path.trim(), alias: c.alias as string | undefined };\n });\n}\n\nfunction parseAggregates(raw: unknown, at: string, name: string): ViewAggregate[] | undefined {\n if (raw === undefined) return undefined;\n if (!Array.isArray(raw)) throw new Error(`${at} (\"${name}\") has \"aggregates\" that is not an array.`);\n return raw.map((entry, j) => {\n const where = `${at} (\"${name}\"), aggregate ${j + 1}`;\n if (!entry || typeof entry !== 'object') throw new Error(`${where} is not an object.`);\n const a = entry as Record<string, unknown>;\n if (typeof a.fn !== 'string' || !AGGREGATE_FNS.includes(a.fn as AggregateFn)) {\n throw new Error(`${where} has an unknown \"fn\". Use one of: ${AGGREGATE_FNS.join(', ')}.`);\n }\n if (typeof a.via !== 'string' || !a.via.trim()) throw new Error(`${where} has no \"via\".`);\n if (a.fn !== 'count' && (typeof a.column !== 'string' || !a.column.trim())) {\n throw new Error(`${where} is a ${a.fn} and needs a \"column\" to summarise.`);\n }\n if (a.on !== undefined && typeof a.on !== 'string') {\n throw new Error(`${where} has an \"on\" that is not a string.`);\n }\n if (a.alias !== undefined && (typeof a.alias !== 'string' || !a.alias.trim())) {\n throw new Error(`${where} has an \"alias\" that is not a non-empty string.`);\n }\n return {\n fn: a.fn as AggregateFn,\n via: a.via.trim(),\n on: a.on as string | undefined,\n column: typeof a.column === 'string' ? a.column.trim() : undefined,\n alias: a.alias as string | undefined,\n };\n });\n}\n\nconst OPS: Op[] = ['=', '!=', '>', '>=', '<', '<=', 'contains', 'startsWith', 'endsWith', 'like', 'in', 'isNull', 'isNotNull'];\n\nfunction parseFilter(raw: unknown, at: string, name: string): ViewFilter | undefined {\n if (raw === undefined) return undefined;\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {\n throw new Error(`${at} (\"${name}\") has a \"filter\" that is not an object.`);\n }\n const groups = (raw as { groups?: unknown }).groups;\n if (!Array.isArray(groups)) throw new Error(`${at} (\"${name}\") has a \"filter\" with no \"groups\" array.`);\n return {\n groups: groups.map((group, g) => {\n if (!Array.isArray(group)) throw new Error(`${at} (\"${name}\"), filter group ${g + 1} is not an array.`);\n return group.map((cond, c) => {\n const where = `${at} (\"${name}\"), filter group ${g + 1} condition ${c + 1}`;\n if (!cond || typeof cond !== 'object') throw new Error(`${where} is not an object.`);\n const x = cond as Record<string, unknown>;\n if (typeof x.path !== 'string' || !x.path.trim()) throw new Error(`${where} has no \"path\".`);\n if (typeof x.op !== 'string' || !OPS.includes(x.op as Op)) {\n throw new Error(`${where} has an unknown operator \"${String(x.op)}\". Use one of: ${OPS.join(', ')}.`);\n }\n if (x.other !== undefined && (typeof x.other !== 'string' || !x.other.trim())) {\n throw new Error(`${where} has an \"other\" that is not a path.`);\n }\n return typeof x.other === 'string'\n ? { path: x.path.trim(), op: x.op as Op, other: x.other.trim() }\n : { path: x.path.trim(), op: x.op as Op, value: x.value as ViewCondition['value'] };\n });\n }),\n };\n}\n\nfunction parseOrder(raw: unknown, at: string, name: string): ViewOrder[] | undefined {\n if (raw === undefined) return undefined;\n if (!Array.isArray(raw)) throw new Error(`${at} (\"${name}\") has an \"orderBy\" that is not an array.`);\n return raw.map((entry, j) => {\n const where = `${at} (\"${name}\"), orderBy ${j + 1}`;\n if (typeof entry === 'string') return { path: entry, direction: 'asc' as const };\n if (!entry || typeof entry !== 'object') throw new Error(`${where} is not a string or an object.`);\n const o = entry as Record<string, unknown>;\n if (typeof o.path !== 'string' || !o.path.trim()) throw new Error(`${where} has no \"path\".`);\n const direction = o.direction === undefined ? 'asc' : o.direction;\n if (direction !== 'asc' && direction !== 'desc') {\n throw new Error(`${where} has a \"direction\" that is not \"asc\" or \"desc\".`);\n }\n return { path: o.path.trim(), direction };\n });\n}\n\n/**\n * Check a view against a schema without running it.\n *\n * Used by `GET /api/views` so a config view naming a table that does not exist\n * on this connection is listed *with its reason*, rather than either vanishing\n * or blowing up the endpoint for every other view beside it.\n */\nexport function validateView(schema: DatabaseSchema, view: ViewDef): string | undefined {\n try {\n /* Any style will do: this compiles to throw away the text and keep the\n error, and no dialect refuses a view another accepts. */\n compileView(schema, view, ANSI_STYLE);\n return undefined;\n } catch (err) {\n return (err as Error).message;\n }\n}\n","/** What core says, in the shell room (packages/kit/src/client/messages.ts says how the catalog is split). */\nimport { defineMessages } from '../../messages.js';\n\nexport const say = defineMessages({\n searchIsNotAnswering: 'Search is not answering right now, so this searched the database instead.',\n isNotAnswering: '{app} is not answering. The server it was started with may have stopped — restart it and reload this page.',\n theServerIsNotAnswering: 'The server is not answering right now. Check your connection, then try again.',\n useACanonicalApi: 'Use a canonical API path without a fragment.',\n invalidOrdinaryUndoIdentity: 'Invalid ordinary Undo identity.',\n yourSessionExpiredReload: 'Your session expired. Reload to continue.',\n theSignedInAccount: 'The signed-in account changed.',\n theServerAnsweredWithSomething: 'The server answered {status} with something that is not JSON. ',\n somethingBetweenThisPage: 'Something between this page and {app} may be intercepting the request.',\n waitForThePending: 'Wait for the pending record update before leaving this view.',\n theTenantContextIsUnavailable: 'The tenant context is unavailable. Verify your session and choose a tenant again.',\n aChangeIsPending: 'A change is pending or unconfirmed. Resolve it before leaving this view.',\n fixTheQueryBefore: 'Fix the query before saving it as a filter.',\n forTheAuthor: '{message} Why: {cause}',\n});\n","/**\n * The parts every other client module needs: the API, the navigation stack,\n * the schema mirrors, and DOM building (dom.js, re-exported here).\n *\n * Apart from independent connection-transition, notification and DOM helpers, this\n * file depends on no other client modules. Everything else depends on it,\n * which is what keeps the module\n * graph a fan-out rather than a web — and lets a feature live in its own file\n * without reaching into the renderer.\n */\n\nimport { flushSync } from 'react-dom';\nimport { DEFAULT_LIMIT } from '@mistriwork/engine/shared/reads.js';\nimport { PAGE_HEADER } from '@mistriwork/engine/shared/pageRoutes.js';\nimport { quoteValue } from '@mistriwork/engine/shared/query.js';\nimport { connectionGeneration } from './connectionTransition.js';\nimport { showToast } from './toast.js';\nimport { scopeApiRequest } from './requestIdentity.js';\nimport { assertSessionCurrent, captureScopedStorage, captureSessionIdentity, sessionIdentity } from './sessionIdentity.js';\nimport { invalidateSession } from './sessionLifecycle.js';\nimport { shell } from './shellMode.js';\nimport { historyToken } from './historyToken.js';\nimport { beginWork } from './pendingWork.js';\nimport { composedReads, setComposedConnection } from './composedHook.js';\nimport { isWritePath } from './writePaths.js';\nimport { firstPaintRead, forgetFirstPaint } from './firstPaint.js';\nimport { ownWriteSent } from './ownWrites.js';\nimport { trailFor } from './recordTrail.js';\nimport { disclosure, el, svgEl, trapFocus } from './dom.js';\nimport {\n columnKind,\n findTable as sharedFindTable,\n labelColumn,\n labelPath as sharedLabelPath,\n looksLikeDate,\n primaryKey,\n referencesFrom,\n referencesTo,\n} from '@mistriwork/engine/shared/schema.js';\nimport { hopName, hopsFrom as sharedHopsFrom, tableAtPrefix as sharedTableAtPrefix } from '@mistriwork/engine/shared/view.js';\nimport { say } from './copy/shell/core.js';\nimport { shellById, shellQuery, shellQueryAll, shellRoot } from './shellHost.js';\nimport { fetchServer } from './transport.js';\nimport { fileReads } from './liveReads.js';\nexport { andQuery, splitClauseTail } from '@mistriwork/engine/shared/query.js';\nexport { humanName } from '@mistriwork/engine/shared/model.js';\n/* Building DOM and the popups' keyboard manners live in dom.js; re-exported\n so every caller keeps its import. */\nexport { disclosure, el, svgEl, trapFocus };\n\n/** @overload @param {'query' | 'table-filter'} id @returns {HTMLInputElement} */\n/** @overload @param {'querybar'} id @returns {HTMLFormElement} */\n/** @overload @param {string} id @returns {HTMLElement} */\n/** @param {string} id */\nexport function $(id) {\n return shellById(id);\n}\n\n/**\n * @type {{schema: any, stack: any[], page: number, canWrite: boolean, activeConnection?: string | null, navMode: string,\n * connections?: (import('@mistriwork/engine/shared/connection.js').AppSource & Partial<import('@mistriwork/engine/shared/connection.js').PublicConnection>)[],\n * onSwitched?: (isCurrent?: () => boolean) => void | Promise<void>}}\n *\n * `stack` is a navigation stack, and each entry is somewhere you have been —\n * a table with a filter, or one row. The breadcrumb is that stack rendered,\n * so walking back restores the filter you walked away from.\n */\nexport const state = { schema: null, stack: [], page: 0, canWrite: false, navMode: 'replace' };\n/* A record page's composed answers (composedHook.ts) go to this connection when a request names none. */\nsetComposedConnection(() => state.activeConnection ?? null);\n\n/* The server's DEFAULT_LIMIT. Small on purpose: the first page of a\n browse should be instant, and paging is one click. */\nconst PAGE_SIZE = DEFAULT_LIMIT;\n\n/**\n * How many rows a page actually holds, which the reader chooses.\n *\n * `PAGE_SIZE` stays the default and stays equal to the server's\n * `DEFAULT_LIMIT` — twenty is right for a first look, and the first look is\n * what most browsing is. It was also the *only* size, which turned a\n * sixty-thousand-row table into three thousand clicks: a table you could read\n * the front of and nothing else.\n *\n * Capped at 250 rather than offering \"all\". The grid renders every row it is\n * given, so the honest ceiling is the one where the DOM is still the reader's\n * friend; past that the answer is a filter, a sort, or Export.\n */\nexport const PAGE_SIZES = [10, 15, 20, 50, 100, 250];\nconst PAGE_SIZE_KEY = 'mistri.page-size';\nlet chosenPageSize = null;\n\nexport function pageSize() {\n if (chosenPageSize === null) {\n const stored = Number(loadJson(PAGE_SIZE_KEY, PAGE_SIZE));\n /* Validated rather than trusted: the value comes from localStorage, where\n a stale build or a hand edit can leave anything at all, and a page size\n of `null` asks the server for `LIMIT NaN`. */\n chosenPageSize = PAGE_SIZES.includes(stored) ? stored : PAGE_SIZE;\n }\n return chosenPageSize;\n}\n\nexport function setPageSize(size) {\n chosenPageSize = PAGE_SIZES.includes(Number(size)) ? Number(size) : PAGE_SIZE;\n saveJson(PAGE_SIZE_KEY, chosenPageSize);\n /* A page *number* means nothing once the size changes — page 40 of twenty\n is not page 40 of a hundred — so the only honest place to land is the\n front. */\n state.page = 0;\n}\n\n/* ---------- API ---------- */\n\n/* An App's search engine did not answer, so a text filter ran on the\n database (the server's `x-mistri-search: database`). Said once per page\n load: the results are still right, only found the slower way. */\nlet searchFallbackSaid = false;\n/** @param {Response} res */\nfunction noteSearchFallback(res) {\n if (searchFallbackSaid || res.headers?.get('x-mistri-search') !== 'database') return;\n searchFallbackSaid = true;\n toast(say('searchIsNotAnswering'));\n}\n\n/**\n * What a reader is told when the server does not answer at all. Offline\n * comes first, because it is the one cause the reader can see and fix; a\n * server on this machine is one they started, so restarting it is theirs to\n * do; anyone else — a hosted App, a phone — can only try again. An App's\n * reader is told the App's name: \"tablewalk\" is the database browser's, and\n * meant nothing to someone working in Compass.\n * @returns {string}\n */\nexport function unansweredSentence() {\n if (typeof navigator !== 'undefined' && navigator.onLine === false) return say('problem.offline');\n const host = typeof location === 'undefined' ? '' : location.hostname;\n if (host === 'localhost' || host === '[::1]' || host.startsWith('127.')) {\n return say('isNotAnswering', { app: appNamed() });\n }\n return say('theServerIsNotAnswering');\n}\n\n/** What an answer that is not JSON says, named by the App a reader is in. */\nexport function uninterpretableSentence(status) {\n return say('theServerAnsweredWithSomething', { status }) + say('somethingBetweenThisPage', { app: appNamed() });\n}\n\n/** The App a reader is in, by its name; in the Workbench, tablewalk. */\nconst appNamed = () => (shell().inApp() && shell().app()?.name) || 'tablewalk';\n\n/* The GET replies the server tags for revalidation (reply.ts sendRevalidated). */\nconst REVALIDATED = new Set(['/api/schema', '/api/app', '/api/pages', '/api/runtime']);\n\n/* What changes a record: a composed page's answers are forgotten when one is sent (writePaths.ts). */\n/** Requests carry the tab's App ID or DB connection ID, never a server-global selection.\n * @template {string} Path\n * @param {Path} path\n * @param {import('@mistriwork/engine/shared/resourceHttp.js').ResourceClientArgs<NoInfer<Path>>} args\n * @returns {Promise<import('@mistriwork/engine/shared/resourceHttp.js').ResourceClientReply<Path>>}\n */\nexport async function api(path, ...args) {\n return sent(path, /** @type {Record<string, unknown> | undefined} */ (args[0]), undefined);\n}\n\n/**\n * A free-form page's own request: `api`, saying which page asks\n * (`x-mistri-page`), so the server refuses a resource or a command the\n * page did not declare. Drift protection, not the boundary: the reader's\n * grants decide the request either way.\n * @param {string} page\n * @param {string} path\n * @param {Record<string, unknown>} body\n * @returns {Promise<any>}\n */\nexport async function pageApi(page, path, body) {\n return sent(path, body, { [PAGE_HEADER]: page });\n}\n\n/**\n * @param {string} path\n * @param {Record<string, unknown> | undefined} body\n * @param {Record<string, string> | undefined} headers\n * @returns {Promise<any>}\n */\nasync function sent(path, body, headers) {\n /* Counted while it is out: a navigation keeps the screen it left until\n the room it opened has had its answers (navigationHold.ts). */\n const settled = beginWork();\n const writing = isWritePath(String(path).split('?')[0], body);\n /* Its echo on the live stream is told from a change someone else made by when it was answered (ownWrites.ts). */\n const answered = writing ? ownWriteSent() : undefined;\n try {\n return await request(path, body, headers);\n } finally {\n answered?.();\n settled();\n /* Forgotten as a write is sent (request) and again as it is answered,\n before its caller refreshes the page: a compose that left while it\n was out may have read the record before the write landed. */\n if (writing) composedReads()?.forget();\n }\n}\n\n/**\n * @param {string} path\n * @param {Record<string, unknown> | undefined} body\n * @param {Record<string, string> | undefined} headers\n * @returns {Promise<any>}\n */\nasync function request(path, body, headers) {\n const parsedPath = new URL(path, window.location.href);\n if (!path.startsWith('/api/') || parsedPath.hash || parsedPath.pathname !== path.split('?')[0]) {\n throw Object.assign(new Error(say('useACanonicalApi')), { outcome: 'not_applied' });\n }\n const identity = captureSessionIdentity();\n const bootstrap = parsedPath.pathname === '/api/whoami' && identity.mode === 'unknown';\n if (!bootstrap) assertSessionCurrent(identity);\n /* A read a record page's compose already asked is answered from it\n (composedReads.ts); a write makes every such answer the past. */\n const composing = composedReads();\n const writing = isWritePath(parsedPath.pathname, body);\n if (writing) forgetFirstPaint();\n if (composing && writing) composing.forget();\n else if (composing && body && composing.may(parsedPath.pathname)) {\n const composed = await composing.reply(parsedPath.pathname, /** @type {Record<string, unknown>} */ (body));\n if (composed !== undefined) {\n assertSessionCurrent(identity);\n return composed;\n }\n }\n if (identity.tenantRequired && parsedPath.pathname !== '/api/whoami' && !parsedPath.pathname.startsWith('/api/auth/')) {\n /* The tenant's lease is the shell's (shellMode.ts): a shell that installed none has no tenant to send through. */\n const tenant = shell().tenant;\n if (!tenant) throw Object.assign(new Error(say('theTenantContextIsUnavailable')), { outcome: 'not_applied' });\n if (tenant.isMutation(parsedPath.pathname)) return await tenant.mutate(path, body, state.activeConnection);\n return await tenant.read(path, body === undefined ? undefined : { ...body }, state.activeConnection);\n }\n if (\n parsedPath.pathname === '/api/revert' &&\n body &&\n 'expectedId' in body &&\n body.expectedId !== undefined &&\n historyToken(body.expectedId, false) === undefined\n )\n throw Object.assign(new Error(say('invalidOrdinaryUndoIdentity')), { outcome: 'not_applied' });\n const { url, payload } = scopeApiRequest(path, body === undefined ? undefined : { ...body }, state.activeConnection);\n\n /* The page arrived with this answer (firstPaint.ts): taken as a reply\n would be, the account it was for held to the session as a reply's is. */\n const carried = firstPaintRead(payload ? 'POST' : 'GET', url, payload);\n if (carried) {\n if (!bootstrap) {\n assertSessionCurrent(identity);\n try {\n assertSessionCurrent(identity, carried.account ?? null);\n if (identity.mode === 'anonymous' && carried.account) throw new Error(say('theSignedInAccount'));\n } catch (error) {\n invalidateSession('account-changed');\n throw error;\n }\n }\n return carried.body;\n }\n\n let res;\n try {\n res = await fetchServer(url, {\n credentials: 'same-origin',\n /* The schema, the App, its pages and the runtime carry an ETag\n (reply.ts sendRevalidated): the browser may keep them, and asks\n again with If-None-Match every time, so a 304 answers an unchanged\n one. Everything else is never stored. */\n cache: !payload && REVALIDATED.has(parsedPath.pathname) ? 'no-cache' : 'no-store',\n method: payload ? 'POST' : 'GET',\n headers: {\n ...(payload ? { 'content-type': 'application/json' } : {}),\n ...(identity.mode === 'authenticated' ? { 'x-mistri-account': identity.accountId } : {}),\n ...headers,\n },\n body: payload ? JSON.stringify(payload) : undefined,\n });\n } catch {\n /* `fetch` rejects with \"Failed to fetch\" or \"Load failed\" depending on the\n browser, and neither says the one thing that is true and useful: the\n server this page came from is not answering. It is a local process, so\n that nearly always means it was stopped — and \"Failed to fetch\" sends\n people to look at their network. Only on this machine, though: a\n hosted App's reader, or one on a phone, cannot restart anything, and\n one who is offline needs to hear that first. */\n throw new Error(unansweredSentence());\n }\n\n if (!bootstrap) {\n assertSessionCurrent(identity);\n if (res.status === 401 && identity.mode === 'authenticated') {\n invalidateSession('expired');\n throw Object.assign(new Error(say('yourSessionExpiredReload')), { status: 401 });\n }\n try {\n assertSessionCurrent(identity, res.headers?.get('x-mistri-account'));\n if (identity.mode === 'anonymous' && res.headers?.get('x-mistri-account')) {\n throw new Error(say('theSignedInAccount'));\n }\n } catch (error) {\n invalidateSession('account-changed');\n throw error;\n }\n }\n noteSearchFallback(res);\n\n let data;\n try {\n data = await res.json();\n } catch {\n /* An answer that is not JSON is a proxy, a login page, or a crash — none\n of which the caller can do anything with, and all of which read better\n as what they are than as \"Unexpected token < in JSON\". */\n throw new Error(uninterpretableSentence(res.status));\n }\n\n if (!bootstrap) assertSessionCurrent(identity);\n /* A refused write answers its envelope — `errors` and a `rejected` outcome — at 422 when the App's rules or a\n value refused it and 409 when the database's keys did (server constraints.ts writeRefusalStatus). The form\n reads it as it reads any refused write, the field marked and the draft kept, so it is an answer here, not a\n failure. */\n if ((res.status === 409 || res.status === 422) && Array.isArray(data?.errors) && data.errors.length && data.outcome === 'rejected')\n return data;\n if (!res.ok) {\n /* The status travels with the message. Callers have always had to guess\n the kind of failure from the text of it, which is how \"No connection is\n open.\" — a 409 with a specific meaning and a specific screen to show\n for it — could only be recognised by matching a sentence. */\n const said = data.message ?? data.error ?? say('common.requestFailed', { status: res.status });\n /* `cause` is sent only to the App's author at work (server\n http/refusalReport.ts): the rule, table or column behind a refusal,\n said beside the caller's sentence wherever that sentence is drawn. */\n const failure = Object.assign(\n new Error(typeof data.cause === 'string' ? say('forTheAuthor', { message: said, cause: data.cause }) : said),\n {\n status: res.status,\n signIn: undefined,\n ...(typeof data.code === 'string' ? { code: data.code } : {}),\n ...(data.outcome === 'not_applied' || data.outcome === 'unknown' ? { outcome: data.outcome } : {}),\n /* The input a command's refusal is about (`refuse(…, { field })`): its form says the sentence beside that field. */\n ...(typeof data.field === 'string' ? { field: data.field } : {}),\n /* A busy server says when to come back (server/http/reply.ts sendBusy), in seconds: kept in ms for a read's retry (components/mount.tsx). */\n ...(res.status === 503 && data.code === 'server_busy' && Number(res.headers?.get('retry-after')) > 0\n ? { retryAfter: Number(res.headers.get('retry-after')) * 1000 }\n : {}),\n /* Where this failure's trace is, while a --dev server's observer runs:\n its badge offers \"Open in observer\" (observe/beacon.js). */\n ...(typeof data.observe === 'string' && /^(\\/[A-Za-z0-9][\\w-]*)*\\/_mistri\\/observe\\//.test(data.observe)\n ? { observe: data.observe }\n : {}),\n // The column names a refusal quotes, for a surface that says them in an App's captions.\n ...(Array.isArray(data.columns) && data.columns.every((column) => typeof column === 'string') ? { columns: data.columns } : {}),\n },\n );\n if (failure.observe) window.dispatchEvent(new CustomEvent('mistri:observe', { detail: failure.observe }));\n /* A server that signs people in answers 401 with what to draw. Told\n once, through a seam, so core does not know what a sign-in screen is. */\n /* A visitor browsing an App's public part is already in the App: a read\n past `auth.public` (a member's list or dashboard) answers 401 as\n `members_only`, and the surface that asked offers the sign-in in place\n (TableRoom's NotYours) rather than the whole page turning into it. */\n if (res.status === 401 && data.signIn) {\n failure.signIn = data.signIn;\n if (!(data.code === 'members_only' && shell().publicVisitor())) unauthorized(data.signIn);\n }\n throw failure;\n }\n fileReads(data, res.headers?.get('mistri-reads'));\n return data;\n}\n\n/**\n * Anything that has to be rebuilt when the database underneath changes.\n *\n * A connection switch is not a render: the trail is cleared, the schema is\n * replaced and every per-connection thing — layouts, saved statements, write\n * mode — is reloaded. Long-lived surfaces that are *not* inside `#content`\n * miss all of it. The SQL editor was the case that showed it: painted once\n * from the state it opened with, it went on naming the previous database\n * while the statement it ran went to the new one, and it kept offering \"Run\n * as write…\" after a switch to a connection the server would refuse.\n *\n * A set rather than the single `state.onSwitched` slot beside it, because the\n * second thing that needed to know would have silently replaced the first.\n */\nconst switchWatchers = new Set();\n\n/** Register a rebuild. Returns the way to stop listening. */\nexport function whenConnectionChanges(run) {\n switchWatchers.add(run);\n return () => switchWatchers.delete(run);\n}\n\n/**\n * Say that the connection changed. Called by the two places that switch —\n * the picker and a deep link into another database — after the new schema\n * and everything keyed to it have landed, so a watcher reads the new state\n * rather than the half-swapped one.\n */\nexport function connectionChanged() {\n /* A copy, because a watcher may well unregister itself as its last act. */\n for (const run of [...switchWatchers]) {\n try {\n run();\n } catch (err) {\n /* One surface failing to rebuild must not stop the next one. It is\n already the wrong database on screen; making it the wrong database\n everywhere is not an improvement. */\n console.error(err);\n }\n }\n}\n\n/* ---------- schema helpers ----------\n\n The shared rules, with the one schema this client holds already applied:\n the browser has one connection open and keeps its schema in `state`, so a\n caller here names a table and not a schema. Each is the server's function\n under the server's name. For a long time they were copies — the client\n could not import the TypeScript — and the copies drifted: a list limit of\n eight against ten, a date used as a row's label on one side and the key on\n the other. Now there is one rule and this is where the client reaches it. */\n\n// React roots may render once while a retired context is being unmounted.\n// An absent catalog has no records or command targets, not a fallback owner.\nexport const findTable = (id) => (state.schema?.tables ? sharedFindTable(state.schema, id) : undefined);\nexport const refsTo = (id) => referencesTo(state.schema, id);\nexport const refsFrom = (id) => referencesFrom(state.schema, id);\nexport { columnKind, labelColumn, primaryKey };\n\n/** Whether a column should be treated as a date for presentation. */\nexport const looksLikeDateColumn = (col) => Boolean(col) && looksLikeDate(col);\n\n/* The zoom-aware ruler is the ui package's (packages/ui/src/internal/ruler.ts says why\n every overlay is measured with it); the client reaches it here, under the\n names it always had. */\nexport { viewportScale, refreshViewportScale, viewportBox, viewportSize } from '@mistriwork/ui/internal/ruler.js';\n\n/** Where a row's name lives, as a view path — see labelPath in shared/schema.ts. */\nexport const labelPath = (table) => sharedLabelPath(state.schema, table, recordLabel(table));\n\n/** An explicit record name, or false when that name is unavailable to this reader. */\nexport const recordLabel = (table) => shell().recordLabel(table.id);\n\n/* ---------- paths ----------\n\n Every hop is derivable from the schema the page already holds, so proposing\n the next segment of a path is local and instant; asking the server per\n keystroke would make the one interaction that has to feel immediate the one\n that does not. The server still resolves every path for real before\n anything runs. */\n\nexport { hopName };\nexport const hopsFrom = (tableId) => sharedHopsFrom(state.schema, tableId);\n/** An empty prefix is the base; a prefix that walks nowhere is null. */\nexport const tableAtPrefix = (baseId, prefix) => sharedTableAtPrefix(state.schema, baseId, prefix) ?? null;\n\n/** How a row is named in a breadcrumb: the readable column, else the key. */\nexport function rowLabel(table, row) {\n /* The walked-to name first. A row fetched as a view carries the joined\n label under its path — `party_id.display_name` — while a plain query row\n simply lacks that key and falls through to the table's own label, then\n the primary key. One function for both, so a caller never has to know\n which kind of row it is holding. */\n const configured = recordLabel(table) !== undefined;\n for (const name of configured ? [labelPath(table)] : [labelPath(table), labelColumn(table)]) {\n const value = name != null ? row[name] : undefined;\n if (value !== null && value !== undefined && String(value) !== '') return String(value);\n }\n const values = primaryKey(table).map((c) => row[c]);\n if (configured && values.some((value) => value == null)) return '(row)';\n const pk = values.filter((value) => value != null);\n return pk.length ? pk.join('/') : '(row)';\n}\n\n/**\n * The primary key of a row, or null when this row cannot supply one.\n *\n * Null rather than a key with `undefined` in it. A projected query — `show\n * quantity`, or a child section showing four summary columns — returns rows\n * that genuinely do not carry the key, and building `{invoice_id: undefined}`\n * out of one produced a link to `~invoice_line:invoice_id=undefined` that\n * landed on \"That row is no longer there.\" Nothing had gone missing; the\n * navigation was built from a row that never had the key to begin with.\n *\n * Callers use the null to decide whether a row is clickable at all, so a row\n * that cannot be opened does not look like one that can.\n */\nexport function keyOf(table, row) {\n const columns = primaryKey(table);\n if (!columns.length) return null;\n const key = {};\n for (const col of columns) {\n if (row[col] === undefined) return null;\n key[col] = row[col];\n }\n return key;\n}\n\n/** A filter matching exactly one row by its primary key. */\nexport function keyFilter(key) {\n return { groups: [Object.entries(key).map(([column, value]) => ({ column, op: '=', value }))] };\n}\n\n/* ---------- navigation ---------- */\n\nlet navigationGeneration = 0;\nexport function invalidateNavigation() {\n navigationGeneration += 1;\n}\n\n/** Capture permission for background work without starting a new navigation intent. */\nexport function captureNavigation() {\n const generation = navigationGeneration;\n const transition = connectionGeneration();\n const connection = state.activeConnection;\n const schema = state.schema;\n const entry = currentEntry();\n return () =>\n generation === navigationGeneration &&\n transition === connectionGeneration() &&\n connection === state.activeConnection &&\n schema === state.schema &&\n entry === currentEntry();\n}\n\n/** A later intent wins even before either request has changed the visible view. */\nexport function beginNavigation() {\n invalidateNavigation();\n return captureNavigation();\n}\n\n/* A paint is a tick of its own beside the walk's: the content pane\n (components/Content.tsx) writes the address bar on every paint and leaves\n the trail's room alone on a crumb correction, which only ticks the walk.\n Flushed synchronously — `render()` has always finished painting before it\n returned, and the address gate (url.js `whileApplying`) and every caller\n that reads the page straight after depend on that. */\nlet paintTick = 0;\nexport const paintVersion = () => paintTick;\n\n/* The room is a third tick, and the one the content pane keys its room by.\n Nearly every paint starts the room over, the way a repaint always did: a\n write that repaints sees itself, another table or record is another room.\n A refinement does not. A page, a page size, or a sort or filter from a\n column header is the same table room asking a different question, and\n the room re-asks it in place (TableRoom's Query is keyed by the\n question), keeping the rows on screen until the answer lands and every\n row decoration's state with them. Only a paint made inside `refine()`\n whose table room is the one already on screen — the same connection,\n depth, table and tab — keeps it; anything else ticks. */\nlet roomTick = 0;\nlet roomOnScreen = null;\nlet refining = false;\nexport const roomVersion = () => roomTick;\n\n/** A table view's identity as a room, or null for anything else. */\nfunction tableRoomShape() {\n const top = currentEntry();\n if (!top || top.kind !== 'table') return null;\n return JSON.stringify([state.activeConnection ?? '', state.stack.length, top.table, top.tab ?? 'rows']);\n}\n\n/**\n * Paint a refinement of the table room on screen: a page, a page size, a\n * header's sort or filter. The address bar and the trail are written as for\n * any paint; the room is kept when it is still the same table room.\n */\nexport function refine(fn) {\n const was = refining;\n refining = true;\n try {\n return fn();\n } finally {\n refining = was;\n }\n}\n\n/* What happens around a paint that may start the room over: the shell\n registers the navigation hold (navigationHold.ts), which keeps the screen\n being left until the new room is complete. A seam, so core stays free of\n the DOM it holds; nothing registered, a paint is only a paint. */\nlet paintHold = null;\n/** @param {{ before(roomChanged: boolean): void; after(): void } | null} hold */\nexport function setPaintHold(hold) {\n paintHold = hold;\n}\n\nexport function render() {\n if (sessionIdentity().mode === 'locked') return;\n paintTick += 1;\n const shape = tableRoomShape();\n const roomChanged = !refining || shape === null || shape !== roomOnScreen;\n if (roomChanged) roomTick += 1;\n roomOnScreen = shape;\n paintHold?.before(roomChanged);\n flushSync(tellWalk);\n paintHold?.after();\n}\n\n/* The walk as something a component can watch. Every render and every\n crumb correction is one tick; a component that subscribes re-reads the\n stack. Kept as a counter rather than a copy of the stack, because the\n stack is mutated in place by design (a label arrives, a root resolves)\n and a copy would miss exactly those corrections. */\nconst walkListeners = new Set();\nlet walkTick = 0;\nfunction tellWalk() {\n walkTick += 1;\n for (const fn of walkListeners) fn();\n}\nexport function subscribeWalk(fn) {\n walkListeners.add(fn);\n return () => walkListeners.delete(fn);\n}\nexport const walkVersion = () => walkTick;\n\n/* Redrawing the trail alone, without re-running the view.\n A row restored from a link knows only its key, so its crumb starts as the\n key and is corrected once the row arrives. Calling render() for that would\n re-fetch the row that just resolved it — and loop. The crumbs are React\n and redraw from the walk's subscribers. */\nexport function renderCrumbs() {\n tellWalk();\n}\n\n/**\n * Move to a view and render.\n *\n * push — a step along a reference; the trail grows\n * replace — the same view, re-run (a sort, an edited filter, a tab)\n * reset — a fresh start; the trail is discarded\n *\n * The distinction is the whole meaning of the breadcrumb. A trail is a walk:\n * every crumb should be reachable from the one before it by following a key.\n * Jumping to an unrelated table is not a step along that walk, so it starts a\n * new one rather than appending browsing history.\n */\nexport function go(entry, mode = 'push') {\n if (!allowMutationExit()) return;\n invalidateNavigation();\n carryView(entry, mode);\n if (mode === 'reset') state.stack = [entry];\n /* Replacing the top of an empty stack — the landing has no entry — wrote\n to index -1: a property on the array, not a member of it. The view\n painted (currentEntry reads the same slot) and the address bar did not\n (the URL walks the array), so the landing's first refinement rendered\n with nothing in the bar to send. An empty stack gains its first entry. */\n else if (mode === 'replace' && state.stack.length) state.stack[state.stack.length - 1] = entry;\n else state.stack.push(entry);\n if (mode !== 'replace') state.page = 0;\n /* How the address bar should record this. A step in the walk earns a\n history entry so the browser's Back button undoes it; a refinement of the\n view you are already on does not, or Back would spend three presses\n undoing sort toggles. */\n state.navMode = mode === 'replace' ? 'replace' : 'push';\n render();\n}\n\n/**\n * Which App view opened a room rides the walk as `view` — its key — and\n * appnav.js reads it where two views share a table. The callers that\n * refine a list (a sort, a header menu, a tab) or open one of its records\n * build a plain `tableView`/`rowView` and know nothing of views, so the\n * walk carries it for them: a same-table refinement in place, and a record\n * of that table opened from it. A caller that names a view — or names\n * none, `view: undefined` — is taken at its word; a fresh start carries\n * nothing.\n */\nfunction carryView(entry, mode) {\n const top = state.stack[state.stack.length - 1];\n if (mode === 'reset' || !top?.view || !entry || 'view' in entry || entry.table !== top.table) return;\n if (entry.kind === 'row' || (entry.kind === 'table' && mode === 'replace')) entry.view = top.view;\n}\n\nexport function backTo(index) {\n if (!allowMutationExit()) return;\n invalidateNavigation();\n state.stack.length = index + 1;\n state.page = 0;\n // Clicking a crumb is a navigation, so Back returns you to where you were.\n state.navMode = 'push';\n render();\n}\n\n/** A table view, optionally filtered. The query text is what the bar shows. */\nexport function tableView(tableId, query, tab) {\n return { kind: 'table', table: tableId, query: query ?? tableId, tab: tab ?? 'rows' };\n}\n\n/**\n * A breakdown: counts and sums, grouped by something.\n *\n * `table` is the base being counted, so the sidebar highlights it and the\n * crumb reads like a table's. It is deliberately not a `tab` of the table\n * view: the rows of a breakdown are groups rather than records, and a tab\n * would promise they behave like the rows beside them.\n */\nexport function breakdownView(tableId, query) {\n return { kind: 'breakdown', table: tableId, query };\n}\n\n/** A single row. The key is enough to re-fetch it, so nothing goes stale. */\nexport function rowView(tableId, key, label) {\n return { kind: 'row', table: tableId, key, label };\n}\n\n/**\n * A page, with or without a root yet.\n *\n * `table` is the page's base rather than a table being browsed, so that the\n * sidebar highlights the right thing and a page can find its way back to a\n * suggested layout when the id names no configured page.\n */\nexport function pageView(pageId, tableId, key, label) {\n return { kind: 'page', pageId, table: tableId, key, label };\n}\n\nexport const currentEntry = () => state.stack[state.stack.length - 1];\n\n// Registered by the draft store: core must not import the editing/rendering tree.\nlet recordMutationPending = () => false;\n/** @param {() => boolean} probe */\nexport function setRecordMutationPendingProbe(probe) {\n recordMutationPending = probe;\n}\n// Transient editors supplement the draft/tenant guards; they never settle an\n// operation merely because its dialog unmounted or the URL did not change.\nconst surfaceMutationProbes = new Set();\n/** @param {() => boolean} probe */\nexport function registerSurfaceMutationPendingProbe(probe) {\n surfaceMutationProbes.add(probe);\n return () => {\n surfaceMutationProbes.delete(probe);\n };\n}\n\n/** Voluntary navigation cannot discard a tenant operation's uncertain result.\n * Forced session retirement bypasses this guard and still clears private data. */\nexport function allowMutationExit() {\n let pending = true;\n try {\n pending = recordMutationPending();\n } catch {\n /* An unreadable owner cannot authorize an exit. */\n }\n if (pending) {\n toast(say('waitForThePending'), 'error');\n return false;\n }\n for (const probe of surfaceMutationProbes) {\n let blocked = true;\n try {\n blocked = probe();\n } catch {\n /* An unreadable surface cannot authorize exit. */\n }\n if (blocked) {\n toast(say('aChangeIsPending'), 'error');\n return false;\n }\n }\n if (!shell().mutations.hasUnresolved() && !shell().accountBusy()) return true;\n toast(say('aChangeIsPending'), 'error');\n return false;\n}\n\n/* How a record opens, when the walk is already looking at pages.\n *\n * A page is a way of reading a record, not a place: someone who chose the\n * page view for a customer wants the invoice they clicked to open the same\n * way, and dropping them into the plain record view — and making them press\n * \"As a page\" again — is the walk changing shape underneath them.\n *\n * The resolver is the shell's (shellMode.ts) rather than imported because\n * core must not know about pages: `page.js` already imports this file, and\n * the preference for a saved or configured page over the suggested one lives\n * there with the rest of the page machinery. No resolver means nothing changes.\n */\n\n/** The page a table's records read through here, or null when unavailable or\n cancelled. Current lookup failures reject; cancelled failures are ignored.\n For a deep link\n that arrives as a row while an app is on: the walk would have opened it\n as a page, and a pasted link should land where a click would. */\nexport async function pageFor(tableId) {\n const pageResolver = shell().resolvePage;\n if (!pageResolver || !shell().pageAffinity()) return null;\n const isCurrent = captureNavigation();\n try {\n const pageId = await pageResolver(tableId);\n return isCurrent() ? (pageId ?? null) : null;\n } catch (error) {\n if (!isCurrent()) return null;\n throw error;\n }\n}\n\n/* Whether pages are the walk's default, not just its current mode.\n *\n * The shell's answer for the same reason the resolver is: an app (appnav.js)\n * reads records through pages by design — that is what makes it an app\n * rather than a browser — but core must not know apps exist. The\n * Workbench's answer is no, and records open the way they always did. */\n\n/** Whether records open as pages here — the sign that an app is reading. */\nexport function prefersPages() {\n return Boolean(shell().pageAffinity());\n}\n\n/* ---------- who is asked to sign in ---------- */\n\nlet unauthorizedShown = false;\n\n/** What happens when the server demands a sign-in: the shell's answer (shellStart.js), once per page load. */\nfunction unauthorized(info) {\n if (unauthorizedShown) return;\n unauthorizedShown = true;\n shell().unauthorized(info);\n}\n\n/* ---------- what may be written ---------- */\n\n/**\n * Whether a table may be written from here, beyond the connection's own\n * flag. In the Workbench, everything the connection allows; an app answers\n * its scope — the tables its manifest names, or nothing — and every write\n * affordance asks before it appears. The server enforces the same list;\n * this is what keeps a button from promising a write the server refuses.\n */\nexport function writeAllowed(tableId) {\n return Boolean(shell().writeScope(tableId));\n}\n\n/* ---------- the empty word ---------- */\n\n/**\n * What an empty value reads as. In the workbench, plain DB mode, it is\n * `null`, because that is the truth about the row and the reader is reading\n * the database. An app answers a dash: a record form full of the word `null`\n * is the schema leaking through, and \"nothing here\" is a form's own word for\n * it, with the truth one hover away. The shell's answer, so core never asks\n * whether an app is on.\n */\nexport function emptyWord() {\n return shell().emptyWord() ?? 'null';\n}\n\n/* ---------- column labels ---------- */\n\n/**\n * The one place a column's shown name is decided, answered by whoever owns\n * the words — app mode answers the manifest's labels plus the humanized\n * fallback; the workbench, plain DB mode, shows the raw name on every\n * surface. One seam instead of a per-surface `inAppMode()` map, because\n * the surfaces that forgot the map were exactly the ones still showing\n * INVOICE_ID in an app.\n */\n\n/**\n * What to call a column of a table right now, or null for \"the raw name\".\n * `column` may be a dotted label path — \"account_id.name\" — and the\n * source decides how that reads. `contextPath` names a related record\n * already identified by the containing field section.\n */\nexport function columnLabel(tableId, column, contextPath = '') {\n return shell().label(tableId, column, contextPath) ?? null;\n}\n\n/**\n * How a column's values dress — the label seam's twin, for rendering.\n * App mode answers the manifest's display rules; in the workbench every\n * value keeps the explorer's plain face. Returns a rule\n * ({ format?, currency?, tone?, bar? }) or null for \"as it is\".\n * @returns {any}\n */\nexport function columnDisplay(tableId, column) {\n return shell().display(tableId, column) ?? null;\n}\n\n/**\n * Open one record, in whichever view the walk is already in.\n *\n * Every route into a record goes through here, so the mode is sticky by\n * default and a call site that wants the record view specifically still says\n * `go(rowView(...))` — which is what the table grid and the record view's own\n * related lists do, since neither is in page view to begin with.\n */\n/* How a record opens can be the app's decision — a drawer, one day a\n modal — the shell's answer, as page affinity is: core stays app-ignorant\n and asks. An opener that answers true has handled the click. Who begins\n reading a record's page ahead of the press that opens it\n (recordCompose.ts) is the shell's answer too. */\n\n/**\n * Begin reading the page a press on this record will open, before the\n * press is over: the same page `openRecord` would choose, asked of the\n * prefetcher, which decides whether the record opens as a page at all.\n * Nothing moves and nothing is drawn; a record that opens some other way\n * asks nothing.\n */\nexport function prefetchRecord(tableId, key) {\n const { prefetchRecord: recordPrefetcher, resolvePage: pageResolver, pageAffinity } = shell();\n if (!recordPrefetcher || !pageResolver || !tableId || !key || typeof key !== 'object') return;\n const here = currentEntry();\n if (here?.kind === 'page' && !here.key && here.table === tableId) {\n recordPrefetcher(here.pageId, tableId, key);\n return;\n }\n if (!(here?.kind === 'page' || pageAffinity(tableId))) return;\n void Promise.resolve(pageResolver(tableId)).then(\n (pageId) => {\n if (pageId) recordPrefetcher(pageId, tableId, key);\n },\n () => {},\n );\n}\n\n/* A glance at one record without leaving the list (Space on a row), which\n peekpanel.js draws: the shell's answer, as the opener is, so core and the\n grid stay free of the drawer. */\n/** Glance at a record over the list; false where the shell draws no drawer. */\nexport function peekRecord(tableId, key, label, records, list) {\n const recordPeeker = shell().peekRecord;\n if (!recordPeeker) return false;\n const branch = unionBranch(tableId, key);\n if (branch) recordPeeker(branch.table, branch.key, label);\n else if (branch === undefined) recordPeeker(tableId, key, label, records, list);\n return true;\n}\n\n/** A union row's own record — `{ table, key }`, null where its branch is not this reader's — or undefined for any other table's. */\nfunction unionBranch(tableId, key) {\n const branches = findTable(tableId)?.union?.branches;\n const branch = branches?.find((b) => b.from === key?.from);\n return branches && (branch ? { table: branch.from, key: { [branch.key]: key.key } } : null);\n}\n\nexport async function openRecord(tableId, key, label, how = 'push', opts = {}) {\n /* A union's row is its branch's record (appUnions.ts): opened there, without the union's list to step through. */\n const branch = unionBranch(tableId, key);\n if (branch !== undefined) return branch ? openRecord(branch.table, branch.key, label, how, { direct: opts.direct }) : undefined;\n if (!allowMutationExit()) return;\n const isCurrent = beginNavigation();\n /* `direct` is the drawer's own \"Details →\" declining its own answer:\n without it the opener would answer the click it just handed over. */\n /* A registered opener may either consume the click (`true`) or ask for a\n row route (`'row'`) while still taking part in the normal navigation\n fencing. The latter is used by app-owned split workspaces: their detail\n pane is a row surface, but automatic page affinity must not replace it\n with a configured page. */\n const answer = !opts.direct ? shell().openRecord(tableId, key, label, opts.records, opts.list) : false;\n /* `{ under }` is a row route that first stands a list beneath it: a split\n picked at the App's root, where the walk is empty (appnav.js). */\n const under = answer && typeof answer === 'object' ? answer.under : undefined;\n const opening = under ? 'row' : answer;\n /* The list it was opened from rides the record's entry, so the record can\n step to its neighbors (recordTrail.ts). Not in the address: a link is to\n the record, and the list it was reached from is this reader's walk. */\n const trail = trailFor(tableId, key, opts.records, opts.list);\n const trailed = (entry) => (trail ? { ...entry, trail } : entry);\n if (opening === true) return;\n /* Rooting the page you are standing on. A rootless page shows its list\n as the chooser, and choosing is not a step of the walk — the entry\n gains its key in place, or the trail reads `invoice › invoice ›\n invoice`, one hollow crumb per change of mind. */\n const here = currentEntry();\n if (here?.kind === 'page' && !here.key && here.table === tableId) {\n go({ ...here, key, label, rootBody: undefined }, 'replace');\n return;\n }\n const pageResolver = shell().resolvePage;\n if (opening !== 'row' && (here?.kind === 'page' || shell().pageAffinity(tableId)) && pageResolver) {\n try {\n const pageId = await pageResolver(tableId);\n if (!isCurrent()) return;\n if (pageId) {\n go(trailed(pageView(pageId, tableId, key, label)), how);\n return;\n }\n } catch {\n /* A page that cannot be resolved is not a reason to go nowhere. */\n }\n }\n if (!isCurrent()) return;\n /* A split's pick is its list's selection, not a step. The first pick\n stands the row on its list — the split's address is the list and then\n the key, `/thread/inbox/~2`, so the list entry stays under it —\n and every later pick replaces that row, as a rooted page gains its key in\n place above: pushed, each pick was one more crumb and one more Back\n (Crumbs.tsx draws the list and its row as one crumb). Only the split's\n own room: a record opened from anywhere else pushes. */\n const picked = opening === 'row' && here?.kind === 'row' && here.table === tableId;\n if (under && !state.stack.length) state.stack.push(under);\n go(trailed(rowView(tableId, key, label)), picked ? 'replace' : how);\n}\n\n/* ---------- chrome shared between views ---------- */\n\n/* Whether the compiled SQL is on show. Held here rather than read off a\n button, because the control that sets it moved into a menu and the pane\n should not care which widget last toggled it. */\nlet explaining = false;\n\nexport const explainVisible = () => explaining;\n\nexport function setExplaining(on) {\n explaining = on;\n showExplain();\n}\n\nexport function setExplain(sql) {\n $('explain').textContent = sql ?? '';\n lastExplained = sql ?? '';\n showExplain();\n}\n\n/* The pane is hidden as a whole rather than the <pre> alone, so the control\n that edits the statement cannot outlive the statement it edits. */\nfunction showExplain() {\n const pane = $('explain-pane');\n if (pane) pane.hidden = !explaining || !$('explain').textContent;\n}\n\n/** The statement the current view compiled to, for anything that wants to run it. */\nlet lastExplained = '';\nexport const explainedSql = () => lastExplained;\n\nexport function showErrors(errors) {\n const box = $('errors');\n box.replaceChildren(...errors.map((e) => el('p', { text: e.message })));\n box.hidden = !errors.length;\n\n /* A query that did not run must not be saveable as a filter.\n \n The banner said `\"quantitee\" is not a column on invoice_line`, correctly\n and helpfully — and beside it \"+ Save this filter\" stayed live, so a typo\n could be given a name and kept. The clause chips stayed ordinary too,\n which is the deeper misdescription: when the query fails *none* of them\n are in effect, so a row of chips reading as the current filter is\n describing something that is not happening.\n\n Done here rather than where the bar is built because this is the only\n moment the app knows: the bar is drawn from the query text, before the\n answer comes back. */\n const failed = errors.length > 0;\n for (const save of shellQueryAll('.view-save')) {\n if (!(save instanceof HTMLButtonElement)) continue;\n save.disabled = failed;\n save.title = failed ? say('fixTheQueryBefore') : '';\n }\n shellQuery('.views')?.classList.toggle('not-in-effect', failed);\n}\n\n/**\n * Quote a value for the query bar, only when it needs it.\n *\n * The one place the client writes a value into the grammar, and it is the\n * server's own `quoteValue`, not a copy: the copy's rule (\"word characters,\n * dots and hyphens are safe\") let a slug carrying `select` through bare, and\n * the parser read it as a clause. `quoteValue` asks the parser whether the\n * bare value reads back as itself, so the two cannot disagree again.\n */\nexport function quote(value) {\n return quoteValue(value);\n}\n\n/** A short-lived message at the top of the content area. */\n/**\n * A passing message, optionally with one thing to do about it.\n *\n * The action exists for the case where the message *is* the moment to act —\n * \"Updated 2 columns\" is exactly when someone realises it was the wrong row,\n * and sending them to a menu to find Undo wastes the seconds in which they\n * still remember what they did. Anything that needs more than one verb belongs\n * in a dialog rather than here.\n */\nexport function toast(message, tone = 'info', action) {\n const host = $('toast');\n if (!host || sessionIdentity().mode === 'locked') return;\n showToast(host, message, tone, action, prefersPages());\n}\n\n/* ---------- persistence ----------\n\n Every localStorage call is wrapped. It throws in Safari's private mode, it\n throws when the quota is exceeded, and it is absent entirely in some\n embedded webviews. A browser that cannot remember your saved filters should\n still let you browse the database — losing a preference is not a reason to\n lose the tool. */\n\nexport function loadJson(key, fallback) {\n try {\n const raw = captureScopedStorage(() => window.localStorage).getItem(key);\n if (!raw) return fallback;\n const parsed = JSON.parse(raw);\n return parsed ?? fallback;\n } catch {\n // Corrupt or unreadable: start fresh rather than breaking the page.\n return fallback;\n }\n}\n\nexport function saveJson(key, value) {\n try {\n captureScopedStorage(() => window.localStorage).setItem(key, JSON.stringify(value));\n return true;\n } catch {\n return false;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"shared-BoLbc2WD.js","names":["say","say","say","say","findTable","sharedFindTable","sharedLabelPath"],"sources":["../../../engine/src/shared/pageRoutes.ts","../../../kit/src/client/connectionTransition.ts","../../../kit/src/client/copy/shell/toast.ts","../../../kit/src/client/toast.ts","../../../kit/src/client/copy/shell/requestIdentity.ts","../../../kit/src/client/requestIdentity.ts","../../../kit/src/client/historyToken.ts","../../../kit/src/client/composedHook.ts","../../../kit/src/client/ownWrites.ts","../../../kit/src/client/recordTrail.ts","../../../kit/src/client/dom.js","../../../engine/src/shared/rollups.ts","../../../engine/src/shared/viewPaths.ts","../../../engine/src/shared/rowScope.ts","../../../engine/src/shared/view.ts","../../../kit/src/client/copy/shell/core.ts","../../../kit/src/client/core.js"],"sourcesContent":["/**\n * A free-form page's two words on the wire, said by the client that sends\n * them and the server that reads them (server/http/pageLoad.ts). Apart from\n * the SDK's module lists so the shell's first chunk carries only these.\n */\n\n/** Where a page's loader is run for the reader: `POST { page, params }`. */\nexport const PAGE_LOAD_PATH = '/api/pages/load';\n/** The header a page's own requests carry: its path. */\nexport const PAGE_HEADER = 'x-mistri-page';\n","/** One owner for picker switches, deep-link switches and schema refreshes. */\n/** Tiny lifetime signal in the existing eager owner. Flag state/validation are\n * imported only by the SDK; an App without components pays no transport cost. */\nlet presentationGeneration = 0,\n presentationAvailable = false;\nconst presentationListeners = new Set<() => void>();\nexport const appPresentation = () => ({ generation: presentationGeneration, available: presentationAvailable });\nexport function replaceAppPresentation(ready = false): void {\n ++presentationGeneration;\n presentationAvailable = ready;\n for (const listener of [...presentationListeners]) {\n try {\n listener();\n } catch {\n /* Observers cannot veto retirement. */\n }\n }\n}\nexport function subscribeAppPresentation(listener: () => void): () => void {\n presentationListeners.add(listener);\n return () => {\n presentationListeners.delete(listener);\n };\n}\nlet generation = 0;\nlet pending = false;\n\nexport const connectionGeneration = (): number => generation;\nexport const connectionTransitionPending = (): boolean => pending;\n\nexport function beginConnectionTransition() {\n replaceAppPresentation();\n const started = ++generation;\n pending = true;\n const isCurrent = () => started === generation;\n return {\n isCurrent,\n finish: () => {\n if (isCurrent()) pending = false;\n },\n };\n}\n","/** What toast says, in the shell room (packages/kit/src/client/messages.ts says how the catalog is split). */\nimport { defineMessages } from '../../messages.js';\n\nexport const say = defineMessages({\n dismissNotification: 'Dismiss notification',\n});\n","import { say } from './copy/shell/toast.js';\n\ninterface ToastAction {\n label: string;\n run: () => unknown;\n}\n\nconst active = new WeakMap<HTMLElement, () => void>();\nconst spoken = new WeakMap<HTMLElement, ReturnType<typeof setTimeout>>();\n\n/* The box is shown and hidden, so it cannot be the live region: a status\n that enters the accessibility tree already holding its message is one a\n screen reader never hears change, and \"Saved\" or \"Moved to Done\" went\n unsaid. The words go to a region that is always there (the box's\n `data-live`), emptied first and filled a beat later, so the same message\n twice is said twice. An error goes to the assertive region (`data-alert`)\n instead: a refused save is news that should interrupt, and a polite\n status waited behind whatever was being read. */\nfunction announce(box: HTMLElement, message: string, urgent: boolean) {\n const id = urgent ? (box.dataset.alert ?? box.dataset.live) : box.dataset.live;\n const live = id ? box.ownerDocument.getElementById(id) : null;\n if (!live) return;\n clearTimeout(spoken.get(live));\n live.textContent = '';\n spoken.set(\n live,\n setTimeout(() => {\n live.textContent = message;\n }, 50),\n );\n}\n\n/** One notification per host, with its timer owned by that notification. */\nexport function showToast(box: HTMLElement, message: string, tone = 'info', action?: ToastAction, app = false) {\n active.get(box)?.();\n const previousFocus = box.ownerDocument.activeElement;\n /* An error does not time out (WCAG 2.2.1): it says what went wrong and\n often what to do, and four seconds was less than it took to read. It\n stays until it is dismissed or replaced. */\n const persists = tone === 'error';\n let remaining = action ? 9000 : 4000;\n let started = 0;\n let timer: ReturnType<typeof setTimeout> | undefined;\n let hovered = false;\n let dismissed = false;\n const focusOut = () => queueMicrotask(resume);\n const pause = () => {\n if (timer === undefined) return;\n clearTimeout(timer);\n timer = undefined;\n remaining = Math.max(0, remaining - (Date.now() - started));\n };\n const dispose = () => {\n dismissed = true;\n pause();\n box.onmouseenter = box.onmouseleave = null;\n box.removeEventListener('focusin', pause);\n box.removeEventListener('focusout', focusOut);\n };\n const dismiss = () => {\n const hadFocus = box.contains(box.ownerDocument.activeElement);\n dispose();\n box.hidden = true;\n if (hadFocus && previousFocus instanceof HTMLElement && previousFocus.isConnected && !box.contains(previousFocus)) {\n previousFocus.focus({ preventScroll: true });\n }\n };\n const resume = () => {\n if (persists || dismissed || hovered || box.contains(box.ownerDocument.activeElement) || timer !== undefined) return;\n started = Date.now();\n timer = setTimeout(dismiss, remaining);\n };\n const span = (className: string, text: string) => {\n const node = box.ownerDocument.createElement('span');\n node.className = className;\n node.textContent = text;\n return node;\n };\n box.className = `toast toast-${tone}`;\n box.replaceChildren();\n if (app) {\n const icon = span('toast-icon', tone === 'ok' ? '\\u2713' : tone === 'error' ? '!' : 'i');\n icon.setAttribute('aria-hidden', 'true');\n box.append(icon);\n }\n box.append(span('toast-message', message));\n announce(box, message, tone === 'error');\n if (action) {\n const button = box.ownerDocument.createElement('button');\n button.type = 'button';\n button.className = 'toast-action';\n button.textContent = action.label;\n button.onclick = () => {\n if (dismissed) return;\n dismiss();\n action.run();\n };\n box.append(button);\n }\n // An error that stays needs a way to go, in the Workbench as in an App.\n if (app || persists) {\n const close = box.ownerDocument.createElement('button');\n close.type = 'button';\n close.className = 'toast-dismiss';\n close.textContent = '\\u00d7';\n close.setAttribute('aria-label', say('dismissNotification'));\n close.title = say('dismissNotification');\n close.onclick = dismiss;\n box.append(close);\n }\n box.onmouseenter = () => {\n hovered = true;\n pause();\n };\n box.onmouseleave = () => {\n hovered = false;\n resume();\n };\n box.addEventListener('focusin', pause);\n box.addEventListener('focusout', focusOut);\n active.set(box, dispose);\n box.hidden = false;\n resume();\n}\n","/** What requestIdentity says, in the shell room (packages/kit/src/client/messages.ts says how the catalog is split). */\nimport { defineMessages } from '../../messages.js';\n\nexport const say = defineMessages({\n noApp: 'No App at /{requested}.',\n invalidAuthenticationRequestIdentity: 'Invalid authentication request identity.',\n databaseConnectionManagementIsUnavailable: 'Database connection management is unavailable to applications.',\n applicationRequestsIdentifyAnApp: 'Application requests identify an app, not a connection.',\n invalidRequestIdentity: 'Invalid {field} request identity.',\n twoDifferentApplications: 'This request names two different applications.',\n twoDifferentConnections: 'This request names two different connections.',\n});\n","import { shell } from './shellMode.js';\nimport { say } from './copy/shell/requestIdentity.js';\n\nlet appConfigured: boolean | null = false;\n\n/** An invalid server contract is a startup failure, not an offline database. */\nexport class RuntimeMetadataError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'RuntimeMetadataError';\n }\n}\n\n/**\n * The address names no App this server serves: `/helpdesk` on a server whose\n * one App is at `/help-desk`. A reader's typo or a stale link, not an outage —\n * the screen says which Apps are here rather than blaming the database.\n */\nexport class UnknownAppError extends Error {\n constructor(\n readonly requested: string,\n readonly apps: ReadonlyArray<{ id: string; name: string }>,\n ) {\n super(say('noApp', { requested }));\n this.name = 'UnknownAppError';\n }\n}\n\n/** Listener identity, independent of the App/Workbench presentation toggle. */\nexport function setAppRequestScope(configured: boolean | null): void {\n appConfigured = configured;\n}\n\nexport const appRequestScope = (): boolean | null => appConfigured;\n\n/* A tenant App's visitor's public address (addressMount.js keepPlacement): every\n request carries it, for it alone places the visitor's reads with a tenant. */\nlet placement: string | null = null;\nexport function setPublicPlacement(via: string | null): void {\n placement = via;\n}\n\n/** The one selector this listener's server reads: `app` on an App server, `conn` in the explorer. */\nconst ownerField = (): 'app' | 'conn' => (appConfigured ? 'app' : 'conn');\n\n/**\n * A request's owner, under the selector the listener reads. Callers capture\n * the owner when their work starts — an App ID on an App listener, a registry\n * ID in the explorer — so an async draft, preview or cached read stays with\n * the source it began on. An absent owner leaves the active one to the\n * transport.\n */\nexport function ownedBy(owner: string | null | undefined): Record<string, string> {\n return owner == null ? {} : { [ownerField()]: owner };\n}\n\nexport function scopeApiRequest(\n path: string,\n body: Record<string, unknown> | undefined,\n active: string | null | undefined,\n): { url: string; payload: Record<string, unknown> | undefined } {\n const url = new URL(path, 'http://mistri.invalid');\n if (url.pathname === '/api/whoami') {\n if (\n url.searchParams.getAll('app').length > 1 ||\n url.searchParams.getAll('via').length > 1 ||\n [...url.searchParams.keys()].some((key) => key !== 'app' && key !== 'via')\n )\n throw new Error(say('invalidAuthenticationRequestIdentity'));\n return { url: `${url.pathname}${url.search}`, payload: body };\n }\n if (url.pathname === '/api/runtime')\n return { url: placement ? `${url.pathname}?via=${encodeURIComponent(placement)}` : url.pathname, payload: body };\n if (/^\\/api\\/connections(?:\\/|$)/.test(url.pathname)) {\n if (appConfigured !== false) throw new Error(say('databaseConnectionManagementIsUnavailable'));\n return { url: path, payload: body };\n }\n if (appConfigured === null) throw new Error(say('common.applicationRuntimeIsUnavailable'));\n\n const field = ownerField();\n // As the server does: an App request names an app, never a connection.\n if (appConfigured && (url.searchParams.has('conn') || body?.conn !== undefined)) throw new Error(say('applicationRequestsIdentifyAnApp'));\n const owners = [...url.searchParams.getAll(field), ...(body?.[field] == null ? [] : [body[field]])];\n if (owners.some((owner) => typeof owner !== 'string' || !owner.trim())) throw new Error(say('invalidRequestIdentity', { field }));\n const distinct = [...new Set(owners as string[])];\n if (distinct.length > 1) throw new Error(say(appConfigured ? 'twoDifferentApplications' : 'twoDifferentConnections'));\n const owner = distinct[0] ?? active;\n let payload = body;\n if (owner) {\n // Commands fingerprint a strict envelope; routing identity belongs to the URL.\n if (appConfigured && url.pathname.startsWith('/api/commands/')) {\n url.searchParams.set('app', owner);\n if (payload) {\n const { app: _app, ...envelope } = payload;\n payload = envelope;\n }\n } else if (payload) payload = { ...payload, [field]: owner };\n else url.searchParams.set(field, owner);\n }\n // Only on a visitor's read, as the shell names them: every other API route refuses an address (server tenantAuthenticatedHttp.ts).\n if (placement && shell().publicRead(url.pathname)) url.searchParams.set('via', placement);\n return { url: `${url.pathname}${url.search}${url.hash}`, payload };\n}\n","export type HistoryToken = number | string;\n\n/** Match tenantRecordHistory's tenantIdentifier(value, 128), not UUID syntax.\n * Validation recognizes a token's shape; only the server can authorize it. */\nexport function historyToken(value: unknown, tenant: boolean): HistoryToken | undefined {\n if (!tenant) return typeof value === 'number' && Number.isSafeInteger(value) && value > 0 ? value : undefined;\n return typeof value === 'string' && value.length > 0 && value.length <= 128 && value === value.trim() && !/\\p{Cc}/u.test(value)\n ? value\n : undefined;\n}\n\n/** One caller-owned connection/context. Never reuse it across owner changes. */\nexport type HistoryQuarantine = Readonly<{ tenant: false; ceiling: number } | { tenant: true; blocked: boolean }>;\n\nexport function createHistoryQuarantine(tenant: boolean): HistoryQuarantine {\n return tenant ? Object.freeze({ tenant: true, blocked: false }) : Object.freeze({ tenant: false, ceiling: 0 });\n}\n\n/** No lexical or numeric ordering exists for opaque tenant tokens. A lost Undo\n * therefore blocks the entire context, including subsequently previewed tokens.\n * An invalid ordinary token cannot establish a safe ceiling: fail closed. */\nexport function quarantineHistory(state: HistoryQuarantine, value: unknown): HistoryQuarantine {\n if (state.tenant === true) return Object.freeze({ tenant: true, blocked: true });\n const token = historyToken(value, false);\n return Object.freeze({ tenant: false, ceiling: Math.max(state.ceiling, typeof token === 'number' ? token : Number.MAX_SAFE_INTEGER) });\n}\n\nexport function historyTokenAvailable(state: HistoryQuarantine, value: unknown): boolean {\n const token = historyToken(value, state.tenant);\n if (token === undefined) return false;\n return state.tenant === true ? !state.blocked : typeof token === 'number' && token > state.ceiling;\n}\n","/**\n * Where the request path (core.js) meets a record page's composed answers\n * (composedReads.ts) without importing them: they tell it of themselves as\n * they load, so a page that opens no record page — the public page — carries\n * none of them. Depends on nothing, so either side may load first.\n */\ninterface ComposedReads {\n /** The answer a compose brought for this request, once. */\n reply(path: string, body: Record<string, unknown>): Promise<unknown>;\n /** Whether a compose kept, or on its way, could answer this path. */\n may(path: string): boolean;\n /** Every answer forgotten: a write makes them the past. */\n forget(): void;\n}\n\nlet reads: ComposedReads | null = null;\n\nexport function setComposedReads(given: ComposedReads): void {\n reads = given;\n}\n\n/** The composed answers, where a module that reads record pages has loaded them. */\nexport const composedReads = (): ComposedReads | null => reads;\n\n/* The connection a request that names none goes to: core.js says, as it loads. */\nlet connection: () => string | null = () => null;\n\nexport function setComposedConnection(source: () => string | null): void {\n connection = source;\n}\n\nexport const composedConnection = (): string | null => connection();\n","/**\n * This page's own writes, for the live stream that echoes them back.\n *\n * Every write the page sends is answered, and its caller then asks again what\n * it changed (recordWriteRefresh.ts). The same write then comes back on the\n * live stream, marked `self` (the reader's own account), a few hundred\n * milliseconds later. Taken as a change someone else made, it asked every read\n * the write's own refresh had just begun once more: a read that began before\n * the echo arrived had, by live sync's rule, begun before the change, so the\n * record page composed twice and every list and count on it was read twice,\n * and the composed answers were forgotten as a write's are, so each section\n * then asked on its own (a field report: 65 to 112 requests after one write).\n *\n * A change is in a read that began after it committed. The page's own write\n * had committed by the time it was answered, so its echo is measured from that\n * moment instead (liveInvalidate.ts): a read begun since already shows it and\n * is left alone, and only a read that began before it (one the write's own\n * refresh did not reach) is asked again. An echo that arrives while a write of\n * the page's is still out waits for its answer, so its refresh and the echo\n * agree on which reads are fresh.\n */\n\n/** How long after the page's own write a `self` change is taken to be that write's echo. Short, since the same\n account writing in another tab within it would have its change taken for this page's: a read this page began\n between the two would not be asked again for it (the next change, focus or the counts' clock asks it). */\nconst ECHO_WINDOW_MS = 3_000;\n/** The longest an echo waits for the page's writes still out to be answered. */\nconst ECHO_WAIT_MS = 10_000;\n\nlet out = 0;\nlet answered: { at: number; mark: number } | undefined;\nlet waiting: Array<() => void> = [];\n/* The moment a write was answered, in the order reads begin in (recordWriteRefresh.ts liveMark). */\nlet marker: () => number = () => 0;\n\n/** Where an answered write stands among the reads (recordWriteRefresh.ts installs it). */\nexport function setOwnWriteMarker(mark: () => number): void {\n marker = mark;\n}\n\n/** A write is going out: call what this returns once it is answered, or has failed. */\nexport function ownWriteSent(): () => void {\n out++;\n let done = false;\n return () => {\n if (done) return;\n done = true;\n out--;\n answered = { at: Date.now(), mark: marker() };\n if (out > 0) return;\n const ready = waiting;\n waiting = [];\n for (const resume of ready) resume();\n };\n}\n\n/** Whether a write of this page's is still out. */\nexport const ownWritesOut = (): boolean => out > 0;\n\n/** Once every write of this page's that is out has been answered, or ECHO_WAIT_MS has passed. */\nexport function ownWritesAnswered(): Promise<void> {\n if (out === 0) return Promise.resolve();\n return new Promise((resolve) => {\n const timer = setTimeout(resolve, ECHO_WAIT_MS);\n waiting.push(() => {\n clearTimeout(timer);\n resolve();\n });\n });\n}\n\n/** Where the page's last write was answered, when that was a moment ago; undefined when no write of the page's is that recent. */\nexport function ownWriteMark(): number | undefined {\n return answered && Date.now() - answered.at < ECHO_WINDOW_MS ? answered.mark : undefined;\n}\n\n/** Forget every write: the session or connection ended. */\nexport function forgetOwnWrites(): void {\n answered = undefined;\n}\n","/**\n * The list a record was opened from, carried so the record can step to its\n * neighbors.\n *\n * Every list already hands `openRecord` the records it is showing (the\n * modal's Previous and Next read them); a grid adds its own question and\n * page. That rides the walk entry as `trail`, so a record page, a peek and\n * the split's detail can say \"3 of 20\" and move one record along — in the\n * list's own order and filter, never a second query of their own. Past the\n * page's edge the list's next or previous page is read with the same\n * question; where the list has no total (an API reports none), a full page\n * is the only sign that more follow, as it is for the pager.\n *\n * Pure: the page reader is passed in, so core.js can import this without a\n * cycle and a test needs no server.\n */\n\nexport interface TrailRecord {\n key: Record<string, unknown>;\n label?: string;\n}\n/** The list's question and the page the records came from. `total` only when it is exact. */\nexport interface TrailList {\n query: string;\n offset: number;\n limit: number;\n total?: number;\n}\nexport interface RecordTrail {\n table: string;\n records: TrailRecord[];\n index: number;\n list?: TrailList;\n}\n/** Reads one page of the list's question, as trail records. */\nexport type TrailPageReader = (table: string, list: TrailList, offset: number) => Promise<TrailRecord[]>;\n\n/**\n * The list a room's answer is one page of, so a record opened from any of\n * its presentations — the grid, cards, a queue, a checklist, a split's list\n * — steps past the page with the same question. Undefined where the answer\n * does not say which page it was. An estimate is no end to stop at: without\n * an exact total, a full page is the sign that more follow — nor is a\n * bounded one (\"10,000+\"), which says only where counting stopped.\n */\nexport function trailListFor(\n query: string,\n page: { limit?: number; offset?: number; total?: number; exactTotal?: boolean; cappedTotal?: boolean },\n): TrailList | undefined {\n if (typeof page.limit !== 'number') return undefined;\n return {\n query,\n offset: page.offset ?? 0,\n limit: page.limit,\n ...(page.exactTotal === false || page.cappedTotal || typeof page.total !== 'number' ? {} : { total: page.total }),\n };\n}\n\n/** A key said the same way whatever its column order or value types. */\nexport const keyText = (key: Record<string, unknown>) =>\n JSON.stringify(\n Object.keys(key)\n .sort()\n .map((name) => [name, String(key[name])]),\n );\n\n/**\n * The trail for a record opened from a list, or undefined where there is\n * nothing to step to: the record is not among the list's records, or it is\n * the only one and no page follows.\n */\nexport function trailFor(\n table: string,\n key: Record<string, unknown>,\n records?: readonly TrailRecord[],\n list?: TrailList,\n): RecordTrail | undefined {\n if (!records?.length) return undefined;\n const index = records.findIndex((record) => keyText(record.key) === keyText(key));\n if (index < 0) return undefined;\n const trail: RecordTrail = {\n table,\n index,\n records: records.map((record) => ({ key: { ...record.key }, ...(record.label === undefined ? {} : { label: record.label }) })),\n ...(list ? { list: { ...list } } : {}),\n };\n return trail.records.length > 1 || canStep(trail, 1) || canStep(trail, -1) ? trail : undefined;\n}\n\n/** Where the record sits: its place in the whole list, and the list's length when it is known. */\nexport function trailPosition(trail: RecordTrail): { at: number; of?: number } {\n const at = (trail.list?.offset ?? 0) + trail.index + 1;\n const of = trail.list ? trail.list.total : trail.records.length;\n return of === undefined ? { at } : { at, of };\n}\n\n/** Whether a step that way leads anywhere. */\nexport function canStep(trail: RecordTrail, step: 1 | -1): boolean {\n if (step === -1) return trail.index > 0 || Boolean(trail.list && trail.list.offset > 0);\n if (trail.index < trail.records.length - 1) return true;\n const list = trail.list;\n if (!list) return false;\n // Without a total, a full page is the only sign that another follows.\n return list.total === undefined ? trail.records.length >= list.limit : list.offset + trail.records.length < list.total;\n}\n\n/**\n * The trail one record along within the page, or undefined where the step\n * leaves it. Synchronous, so a step that reads nothing lands at once.\n */\nexport function stepWithin(trail: RecordTrail, step: 1 | -1): RecordTrail | undefined {\n const next = trail.index + step;\n return next >= 0 && next < trail.records.length ? { ...trail, index: next } : undefined;\n}\n\n/**\n * The trail one record along. Within the page it is a move of the index;\n * past it, the list's next (or previous) page is read and the step lands on\n * its first (or last) record. Undefined when there is nowhere to go, which\n * a reader answering an empty page also says.\n */\nexport async function stepTrail(trail: RecordTrail, step: 1 | -1, read: TrailPageReader): Promise<RecordTrail | undefined> {\n const within = stepWithin(trail, step);\n if (within) return within;\n if (!trail.list || !canStep(trail, step)) return undefined;\n const offset = Math.max(0, trail.list.offset + step * trail.list.limit);\n const records = await read(trail.table, trail.list, offset);\n if (!records.length) return undefined;\n return { ...trail, records, index: step === 1 ? 0 : records.length - 1, list: { ...trail.list, offset } };\n}\n","/**\n * Building DOM, and the keyboard manners of the page's own popups: a\n * disclosure that Escape and an outside click close, and a trap that keeps\n * Tab inside a panel and hands the focus back on the way out.\n *\n * Out of core.js, which re-exports all four so every caller keeps its\n * import. Nothing here reads the walk, the schema or the API, so this module\n * imports nothing and core.js can import it without a cycle.\n */\nimport { addHostListener, removeHostListener, shellQuery } from './shellHost.js';\n\nexport function el(tag, attrs = {}, children = []) {\n const node = document.createElement(tag);\n for (const [k, v] of Object.entries(attrs)) {\n if (v === undefined || v === false || v === null) continue;\n if (k === 'class') node.className = v;\n else if (k === 'text') node.textContent = v;\n else if (k.startsWith('on')) node.addEventListener(k.slice(2), v);\n else node.setAttribute(k, v === true ? '' : String(v));\n }\n for (const child of [].concat(children)) {\n if (child == null || child === false) continue;\n node.append(child);\n }\n return node;\n}\n\nexport function svgEl(tag, attrs = {}, children = []) {\n const node = document.createElementNS('http://www.w3.org/2000/svg', tag);\n for (const [k, v] of Object.entries(attrs)) {\n if (v === undefined || v === null) continue;\n if (k.startsWith('on')) node.addEventListener(k.slice(2), v);\n else node.setAttribute(k, String(v));\n }\n for (const child of [].concat(children)) if (child) node.append(child);\n return node;\n}\n\n/**\n * Make a `<details>` announce itself as the control it looks like.\n *\n * A `<summary>` styled as a button is read by Chromium as `group: + Count` —\n * no role, no state, nothing to say it can be opened or that it currently is.\n * The reader sees a button; the screen reader is told about a grouping box.\n * Every disclosure in this app is a button in appearance and in intent, so\n * each one says so, and keeps `aria-expanded` in step with the element's own\n * `open` — through the `toggle` event, because a `<details>` can be opened by\n * the keyboard, by a click on the marker, or by `open` being set in code, and\n * only the event sees all three.\n */\nexport function disclosure(details, { dismissible = true } = {}) {\n const summary = details.querySelector(':scope > summary');\n if (!summary) return details;\n summary.setAttribute('role', 'button');\n summary.setAttribute('aria-expanded', String(details.open));\n /* Light dismissal, here so every disclosure gets it: the Export menu was\n the one popup on the page that Escape and an outside click both left\n open. Escape closes only this layer — stopped, the way every stacked\n surface here learned to — and hands focus back to the summary. */\n const outside = (e) => {\n if (!details.contains(e.target)) details.open = false;\n };\n const onKey = (e) => {\n if (e.key !== 'Escape') return;\n e.preventDefault();\n e.stopPropagation();\n details.open = false;\n summary.focus();\n };\n details.addEventListener('toggle', () => {\n summary.setAttribute('aria-expanded', String(details.open));\n if (!dismissible) return;\n if (details.open) {\n addHostListener(document, 'pointerdown', outside, true);\n addHostListener(document, 'keydown', onKey, true);\n } else {\n removeHostListener(document, 'pointerdown', outside, true);\n removeHostListener(document, 'keydown', onKey, true);\n }\n });\n return details;\n}\n\n/**\n * Keep the keyboard inside a panel, and give it back on the way out.\n *\n * Eight surfaces here declare `role=\"dialog\"` and `aria-modal=\"true\"` on a\n * plain `<div>`. The attribute is a promise: it tells a screen reader that\n * everything behind this is inert and not to be reached. Nothing was\n * enforcing it, so Tab walked straight out of the panel and into the table\n * underneath — the page reading as sealed to one user and open to another,\n * which is worse than not claiming it at all. The `<dialog>` elements\n * elsewhere get this from the platform; these had to be told.\n *\n * It also puts focus back where it came from. Closing a panel used to drop\n * focus on `<body>`, so the next Tab started again from the top of the\n * document — a keyboard user who opened the connection manager from the\n * toolbar and pressed Escape had to walk the whole page to get back.\n *\n * Returns the release function. Call it when the panel goes.\n */\nexport function trapFocus(panel) {\n const cameFrom = document.activeElement;\n\n /* Queried per keystroke rather than cached: these panels rebuild their own\n contents constantly — a filter that repaints a list, a tour that swaps\n the card — and a list captured on open goes stale immediately. */\n const focusable = () =>\n [\n ...panel.querySelectorAll(\n 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]),' +\n ' textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])',\n ),\n ].filter((node) => node.offsetWidth > 0 || node.offsetHeight > 0 || node === document.activeElement);\n\n /* Focus in a modal opened over this panel is that modal's to keep. Since\n Modal.tsx moved every dialog onto Base UI there is no `<dialog open>` to\n see: a modal is an `aria-modal` element on a portal of its own, and this\n trap pulled focus back from it while Base UI's pulled it in again — the\n connection form's name, filled as it opened, landed in the URL box. */\n const aboveUs = (node) => {\n if (shellQuery('dialog[open]')) return true;\n const owner = node instanceof Element ? node.closest('[aria-modal=\"true\"]') : null;\n return Boolean(owner && owner !== panel && !owner.contains(panel));\n };\n\n const onKey = (e) => {\n if (e.key !== 'Tab') return;\n if (aboveUs(document.activeElement)) return;\n const stops = focusable();\n /* Nothing to move to yet — a panel painted but not filled. Swallowing the\n key is the honest answer: the alternative is Tab escaping into the page\n this panel has told a screen reader is inert. Escape still closes. */\n if (!stops.length) {\n e.preventDefault();\n return;\n }\n const first = stops[0];\n const last = stops[stops.length - 1];\n /* Focus outside the panel entirely — it can happen, because a panel may\n be opened while something behind it is focused. Pull it back rather\n than letting the first Tab escape. */\n if (!panel.contains(document.activeElement)) {\n e.preventDefault();\n (e.shiftKey ? last : first).focus();\n return;\n }\n if (e.shiftKey && document.activeElement === first) {\n e.preventDefault();\n last.focus();\n } else if (!e.shiftKey && document.activeElement === last) {\n e.preventDefault();\n first.focus();\n }\n };\n\n /**\n * Focus that ends up outside, brought back.\n *\n * Tab is not the only way out. These panels repaint themselves — the\n * connection list arrives and replaces the loading state, a filter redraws\n * the rows — and replacing the node that had focus drops it on `<body>`,\n * outside the thing that claims to be modal. A click on the page behind\n * does the same.\n *\n * A modal on top is the exception, and it has to be: the connections\n * panel opens one to add a connection, and pulling focus back to the panel\n * underneath would make that dialog impossible to type into. The modal\n * keeps its own focus, so this stands aside.\n */\n const onFocusIn = (e) => {\n if (!panel.isConnected) return;\n if (panel.contains(e.target)) return;\n if (aboveUs(e.target)) return;\n const stops = focusable();\n if (stops.length) stops[0].focus();\n };\n\n /**\n * The panel rebuilding itself out from under the keyboard.\n *\n * This is the one that actually bit. These panels paint twice — a loading\n * state, then the answer — and `replaceChildren` destroys whatever had\n * focus. Removing a focused element does not reliably fire `blur` or\n * `focusout` in Chrome, so nothing above hears it; focus simply becomes\n * `<body>` and the next Tab starts from the top of the document, outside\n * the panel that claims to be modal.\n *\n * Watching the subtree catches it whatever caused it — a repaint, a row\n * that removed itself, a list that filtered. If the panel put focus\n * somewhere itself, as the connection filter does, this sees focus already\n * inside and stands aside.\n */\n const watcher = new MutationObserver(() => {\n if (!panel.isConnected) return;\n if (panel.contains(document.activeElement)) return;\n if (aboveUs(document.activeElement)) return;\n const stops = focusable();\n if (stops.length) stops[0].focus();\n });\n watcher.observe(panel, { childList: true, subtree: true });\n\n addHostListener(document, 'keydown', onKey, true);\n addHostListener(document, 'focusin', onFocusIn);\n return () => {\n watcher.disconnect();\n removeHostListener(document, 'keydown', onKey, true);\n removeHostListener(document, 'focusin', onFocusIn);\n /* Only if the panel still holds it. Something that closed itself by\n sending you somewhere — a connection row that opens that database —\n has already put focus where it belongs. */\n if (cameFrom?.isConnected && !document.activeElement?.closest?.('dialog')) {\n if (cameFrom instanceof HTMLElement && (!panel.isConnected || panel.contains(document.activeElement))) cameFrom.focus();\n }\n };\n}\n","/**\n * Rollups: one number about the rows that point at a record — an order's\n * line total, an account's open deals — declared once on the resource\n * (`rollups: { line_total: sum('order_line', 'amount') }`) and read wherever\n * a column of the record is: a list's `show`, a sort, a tab's condition, a\n * record page's fields, a card's figure and facts, a queue's facts, a\n * picker's query, a dashboard's measure and a breakdown's key — and through\n * a reference, as `sales_order_id.lines`, wherever a path is.\n *\n * A rollup is never a column of the table. It is not in `TableMeta.columns`,\n * so every path that reads one table by its own columns — an insert form, a\n * plain `/api/query`, an export of the table, a filter box built from the\n * schema — never sees it and never sends it to a single-table read, which\n * would be a statement naming a column the database does not have. It lives\n * beside the columns (`TableMeta.rollups`), where the path resolver finds it\n * by name, and it is compiled only by the view compiler, as a correlated\n * scalar subquery through one reference: the same construction as a view's\n * aggregates (viewPaths.ts says why a subquery rather than a grouped join),\n * so a list still has one row per record however many rollups it shows.\n *\n * The child rows are read through the statement's own relations\n * (rowScope.ts), so a row policy or a tenant scope on the child narrows the\n * number exactly as it narrows a related list of the same rows, and a reader\n * whose catalog cannot see the child table, its column or the reference is\n * served no rollup at all (appRollupRules), rather than a number built from\n * rows they may not read.\n */\nimport type { Filter } from './filter.js';\nimport type { ComputedColumn } from './computed.js';\nimport type { ColumnMeta } from './schema.js';\n\nexport const ROLLUP_FNS = ['count', 'sum', 'min', 'max', 'latest'] as const;\nexport type RollupFn = (typeof ROLLUP_FNS)[number];\n\n/** A rollup as the catalog serves it and a view carries it: no request ever supplies one. */\nexport interface Rollup {\n fn: RollupFn;\n /**\n * The reference from the child rows to the record, as `child.column`\n * (`+`-joined for a composite key): endpoints rather than a constraint\n * name, so a layer that renames tables (an App's logical names, a\n * source's prefix) can rewrite it without resolving anything.\n */\n via: string;\n /** The child's column; absent for a count. */\n column?: string;\n /**\n * A `latest`'s order: the child's column whose greatest value is the latest row (its key breaks a tie), and\n * whose `column` the rollup reads — a payment's status, the last visit's outcome. A word about the rows, not a\n * figure, so it is never summed. Through two references it is the inner level's: the outer reads the newest of\n * all the rows joined to their middle record, not the newest of each middle's newest (viewLabels.ts).\n */\n by?: string;\n /**\n * A `latest` that reads a reference column (the newest lease's `resident_id`): the record it points at, so the value\n * is drawn as that record's name and link and never as its key (referenceModel.ts).\n */\n references?: { table: string; column: string };\n /**\n * Set only on a rollup a path reads *through* (`resident.name`, viewPaths.ts): the value wanted is this column of the\n * record the rollup's key names, read through that table's own relation, so a row policy narrows it as it narrows a\n * join to the same record. The reader must be able to read both (rollupReads).\n */\n through?: { table: string; key: string; column: string };\n /** A `latest`'s vocabulary: the child column's own, so the word it reads is drawn, filtered and typed as that column is. */\n allowed?: string[];\n /**\n * Set when `column` names a computed column of the child (computed.ts)\n * rather than a stored one: its expression, constants folded, written\n * inline over the child's row inside the aggregate — the same SQL a list\n * of the child writes for it. It reads the child's own columns only, never\n * a rollup or a reference (appRollupRules refuses those), so a rollup over\n * it is still one subquery, and no rollup can come to read itself.\n */\n expression?: Pick<ComputedColumn, 'type' | 'expr' | 'reads'>;\n /** Which child rows count — the rollup's `query`, over the child's own columns and paths through its references. */\n filter?: Filter;\n /**\n * The computed columns of the child that `filter` names (`breached = true`), each its expression, constants\n * and `now` folded: written over the row inside the narrowed relation, as `expression` is inside the aggregate.\n * Each reads the child's own columns only (appRollupRules), so the figure is still one subquery.\n */\n computed?: Record<string, Pick<ComputedColumn, 'type' | 'expr' | 'reads'>>;\n /**\n * The tables a path in `filter` walks to, with the columns read there (`sales_order_id.status` reads\n * `sales_order.id` and `sales_order.status`): what a reader must be able to read for the figure to be\n * served (rollupReads), as the child's own columns are.\n */\n reach?: Array<{ table: string; fields: string[] }>;\n /** The value's type: an integer for a count, the column's own otherwise. */\n type: string;\n /**\n * Set when `column` names a rollup of the child rather than one of its\n * columns — a customer's `lines: sum('sales_order', 'lines')`, the count\n * of its orders' lines, which is how a rollup reaches two hops. The inner\n * rollup is compiled inside the outer one's aggregate, each through its own\n * child's relation, so a row policy narrows both levels. One level only:\n * the inner one reads a column (appRollupRules says so when it does not).\n *\n * A rollup whose `whose` walks two references (`sum('time_entry', 'hours',\n * { whose: 'task_id.project_id' })`) is served in the same shape: the outer\n * reads the middle table (`task.project_id`) with no column of its own,\n * the inner the rows and column declared, with the declared query. A count\n * so built is the sum of the inner counts, and 0 where there are none; a\n * max of maxes and a min of mins are the max and min of the rows; a latest\n * is one ordered read over the rows joined to the middle table. The\n * access layer holds a reader to both levels' tables and fields\n * (rollupReads), as it does a rollup of a rollup.\n */\n of?: Rollup;\n}\n\n/**\n * A rollup a view names, keyed by its path in `ViewDef.computed`: the rollup\n * and the path prefix of the record it hangs off ('' for the view's base).\n */\nexport interface ComputedPath extends Rollup {\n on: string;\n}\n\n/** A rollup read as a column: never written, null only where there are no rows to read (a count is 0). */\nexport function rollupColumnMeta(name: string, rollup: Pick<Rollup, 'fn' | 'type' | 'allowed' | 'references'>): ColumnMeta {\n return {\n name,\n type: rollup.type,\n nullable: rollup.fn !== 'count',\n primaryKey: false,\n writable: false,\n ...(rollup.allowed ? { allowed: rollup.allowed } : {}),\n ...(rollup.references ? { references: rollup.references } : {}),\n };\n}\n\n/** The child table a rollup's `via` names. */\nfunction rollupChild(via: string): string {\n return via.slice(0, via.lastIndexOf('.'));\n}\n\n/** `via` with its child table renamed — a layer mapping logical names to physical ones. */\nfunction renamedVia(via: string, rename: (table: string) => string): string {\n const dot = via.lastIndexOf('.');\n return `${rename(via.slice(0, dot))}${via.slice(dot)}`;\n}\n\n/** A rollup with every child table it reads renamed, the inner one's too. */\nfunction renamedRollup<R extends Rollup>(rollup: R, rename: (table: string) => string): R {\n return {\n ...rollup,\n via: renamedVia(rollup.via, rename),\n ...(rollup.reach ? { reach: rollup.reach.map((read) => ({ ...read, table: rename(read.table) })) } : {}),\n ...(rollup.references ? { references: { ...rollup.references, table: rename(rollup.references.table) } } : {}),\n ...(rollup.through ? { through: { ...rollup.through, table: rename(rollup.through.table) } } : {}),\n ...(rollup.of ? { of: renamedRollup(rollup.of, rename) } : {}),\n };\n}\n\n/** A view's or a breakdown's `computed` with every child table renamed. */\nexport function renamedComputed<T extends { computed?: Record<string, ComputedPath> }>(def: T, rename: (table: string) => string): T {\n return def.computed\n ? { ...def, computed: Object.fromEntries(Object.entries(def.computed).map(([path, rollup]) => [path, renamedRollup(rollup, rename)])) }\n : def;\n}\n\n/**\n * Every table a rollup reads rows of, with the fields it reads there: the\n * reference it walks, the column it sums (not an inner rollup's name, which\n * is no field, nor a computed column's, whose own reads stand for it) and\n * the columns its query narrows by — the inner rollup's too. What an access\n * layer holds a reader's grants against.\n */\nexport function rollupReads(rollup: Rollup): Array<{ table: string; fields: string[] }> {\n const table = rollupChild(rollup.via);\n const fields = [\n ...rollup.via.slice(table.length + 1).split('+'),\n ...(rollup.expression ? rollup.expression.reads : rollup.column && !rollup.of ? [rollup.column] : []),\n ...(rollup.by === undefined ? [] : [rollup.by]),\n // A path's first hop is the child's own reference column; the rest is in `reach`.\n ...(rollup.filter?.groups ?? []).flatMap((group) =>\n group.flatMap((c) =>\n [c.column, ...(c.other === undefined ? [] : [c.other])].flatMap((name) =>\n // A computed column's own reads stand for it, as `expression`'s do.\n rollup.computed && Object.hasOwn(rollup.computed, name) ? rollup.computed[name].reads : name.split('.')[0].split('+'),\n ),\n ),\n ),\n ];\n return [\n { table, fields },\n ...(rollup.reach ?? []),\n ...(rollup.of ? rollupReads(rollup.of) : []),\n ...(rollup.through ? [{ table: rollup.through.table, fields: [rollup.through.key, rollup.through.column] }] : []),\n ];\n}\n","/*\n * The view model and its reference paths: what a view holds, and how a path\n * or an aggregate resolves against the foreign-key graph to the tables and\n * columns it names. view.ts compiles what resolves here into SQL.\n */\nimport { type AnyRows, type Op } from './filter.js';\nimport { findTable, isJsonType, referencesFrom, type ColumnMeta, type DatabaseSchema, type ForeignKey, type TableMeta } from './schema.js';\nimport { nearest, shortList } from './suggest.js';\nimport { LABEL_SEGMENT } from './recordLabel.js';\nimport { rollupColumnMeta, type ComputedPath, type Rollup } from './rollups.js';\nimport { computedColumnMeta, type ComputedColumn, type ComputedPath as ExpressionPath } from './computed.js';\nimport { TAGS_COLUMN, tagsColumnMeta, type TagPath, type TagSet } from './tagLists.js';\nimport { DEFINED_KEY, definedColumnMeta } from './definedFields.js';\n\n/* ---------- the model ---------- */\n\n/** One column of a view: a path, and optionally what to call it. */\nexport interface ViewColumn {\n path: string;\n /** Output name. Defaults to the path itself, which is already unique. */\n alias?: string;\n}\n\n/** One condition. Identical to `Condition` except that it addresses a path. */\nexport interface ViewCondition {\n path: string;\n op: Op;\n value?: string | number | boolean | Array<string | number>;\n /** As on `Condition`: compare by bytes; only comparisons take it. */\n collation?: 'binary';\n /** As on `Condition`: compare with this path's value rather than `value`. */\n other?: string;\n /**\n * As on `Condition`: the rows that point at this one, asked whether any\n * matches. `path` is then the condition as it is written (`any opportunity\n * where stage = won`), for a reader to say back; the compiler reads `any`.\n */\n any?: AnyRows;\n}\n\n/** OR-of-ANDs over paths — the same shape as `Filter`, one level out. */\nexport interface ViewFilter {\n groups: ViewCondition[][];\n}\n\nexport interface ViewOrder {\n path: string;\n direction: 'asc' | 'desc';\n /**\n * When true, `path` names an aggregate's output alias rather than a\n * column: `sort invoices desc` after `count invoice … as invoices`.\n * Compiled as the quoted alias, which every dialect here accepts in an\n * ORDER BY — the one clause that runs late enough to see it.\n */\n alias?: boolean;\n /**\n * Closest first: order by the distance of `path` from this number rather\n * than by `path` itself — a similar list's nearest price. Written into the\n * ORDER BY as a literal, so it must be a finite, safe number; anything\n * else is refused, never quoted.\n */\n near?: number;\n /**\n * Order text by its bytes, not by the column's collation, so that a keyset\n * cursor's comparison (`collation` on a condition) agrees with the order it\n * resumes. A column only: an alias or a distance is refused with it.\n */\n collation?: 'binary';\n /**\n * Nulls after every value, in either direction, on every dialect: the order\n * a keyset cursor over a nullable column resumes (apiReadBoundary).\n */\n nulls?: 'last';\n}\n\n/* ---------- the other direction ----------\n\n A reference path walks *forwards*, along a foreign key from the row that\n holds it, so it always lands on at most one row. That is what lets a view\n promise one row per base record.\n\n The backwards direction — the rows that point *at* you — is the more\n interesting question and the one the tool is built around, but it is\n many-valued, and joining it multiplies: a work order with four parts and\n three notes becomes twelve rows, each repeating the work order's own\n columns, and the number on screen stops meaning work orders. Almost nobody\n asking for \"work orders with their parts\" wants that.\n\n An aggregate is the honest one-row-per-record answer to a many-valued\n question. It compiles to a correlated scalar subquery, which is worth\n preferring over a grouped join for three reasons: cardinality cannot change\n no matter how many aggregates are added, each one is independent so two\n counts over different children cannot inflate each other, and it carries no\n parameters — so the placeholder numbering that Postgres cares about is\n untouched by however many of these a view collects. */\n\nexport type AggregateFn = 'count' | 'sum' | 'min' | 'max' | 'avg';\n\nexport interface ViewAggregate {\n fn: AggregateFn;\n /**\n * The foreign key pointing back at the parent, by constraint name.\n *\n * A name rather than a table, because a table can point at the same parent\n * more than once — `work_order.parent_id` and `work_order.supersedes_id`\n * are two different questions and \"count of work_orders\" is neither.\n */\n via: string;\n /** Path prefix naming the parent the children point at. Empty is the base. */\n on?: string;\n /** Column on the child table. Required for everything except `count`. */\n column?: string;\n alias?: string;\n}\n\nexport interface ViewDef {\n id: string;\n name: string;\n /** Base table id. Every path in the view starts here. */\n base: string;\n columns: ViewColumn[];\n /** Scalar summaries of many-valued relationships. */\n aggregates?: ViewAggregate[];\n filter?: ViewFilter;\n orderBy?: ViewOrder[];\n limit?: number;\n /**\n * The view written down, in the same language.\n *\n * When present it is the definition and the structure above is derived from\n * it — a config file holding `work_order show reference, customer_id.name\n * as customer count work_order_part via work_order_id as parts` is a file\n * someone can review in a pull request, which a nest of JSON objects with\n * `op: \"contains\"` in it is not.\n */\n text?: string;\n /** Config-defined views may name the connection they belong to. */\n connection?: string;\n /** Where this came from. Absent means the client's own storage. */\n source?: 'config' | 'saved';\n /**\n * The record-label templates its `$label` paths compile from, by the path\n * prefix each hangs off ('' for the base's own). Filled by the server's\n * read from the App's catalog (recordLabel.ts); a request never supplies it.\n */\n labels?: Record<string, string>;\n /**\n * The rollups its paths name (rollups.ts), by path: what makes `line_total`\n * or `customer_id.open_deals` a correlated subquery rather than a column.\n * Filled by the server's read from the App's catalog, like `labels`, and\n * carried with the view so a layer whose catalog is the database's own\n * (an adapter, a source) compiles the same statement; a request never\n * supplies it.\n */\n computed?: Record<string, ComputedPath>;\n /**\n * The computed columns its paths name (computed.ts), by path, each with its\n * expression and the prefix of the record it is on: filled from the App's\n * catalog like `computed`, so an adapter writes the same expression; a\n * request never supplies it.\n */\n expressions?: Record<string, ExpressionPath>;\n /**\n * The record tags its paths name (tagLists.ts), by path: filled from the\n * App's catalog like `computed`, so an adapter writes the same subquery; a\n * request never supplies them.\n */\n tagged?: Record<string, TagPath>;\n /**\n * The App's declared references (soft keys) the view may walk, as edges of\n * the catalog it is compiled against (declaredReferences.ts). Attached by\n * the App's resource layer, where a catalog that is the database's own\n * does not have them; a request never supplies them.\n */\n references?: ForeignKey[];\n}\n\n/**\n * How many foreign keys a path may follow.\n *\n * A cap is not a nicety: `work_order.parent_id` points at `work_order`, so the\n * path space is infinite and any breadth-first enumeration of it runs forever.\n * Four hops is past the point where anyone can still read the result, and the\n * limit is stated in the error rather than left to be discovered.\n */\nexport const MAX_PATH_DEPTH = 4;\n\n/** What a `$label` path ends on: text, and empty where the row is missing. */\nexport const labelColumnMeta = (): ColumnMeta => ({ name: LABEL_SEGMENT, type: 'TEXT', nullable: true, primaryKey: false });\n\n/* ---------- errors ---------- */\n\n/**\n * A path that does not resolve, with the segment that failed.\n *\n * The segment matters more than the path: `customer_id.country_code.nmae` is\n * four words of which one is wrong, and an error that repeats the whole path\n * makes you find the wrong one yourself.\n */\nexport class PathError extends Error {\n constructor(\n message: string,\n readonly path: string,\n readonly segment: string,\n /** Index of the failing segment within the path, 0-based. */\n readonly index: number,\n ) {\n super(message);\n this.name = 'PathError';\n }\n}\n\n/* ---------- hops ---------- */\n\n/**\n * What a hop is called in a path.\n *\n * A single-column foreign key is named by its column, which is what anyone\n * would write and what the composer shows. A composite key has no single\n * column to name it, so its columns are joined with `+` — deterministic,\n * derivable from the model alone, and impossible to confuse with a column\n * name because `+` is not legal in one.\n */\nexport function hopName(fk: ForeignKey): string {\n return fk.from.columns.length === 1 ? fk.from.columns[0] : fk.from.columns.join('+');\n}\n\nexport interface Hop {\n /** What to write in a path. */\n name: string;\n fk: ForeignKey;\n /** The table this hop lands on. */\n target: string;\n /** True when every column of the key is nullable — the row may not exist. */\n optional: boolean;\n}\n\n/** Every forward hop available from a table, in a stable order. */\nexport function hopsFrom(schema: DatabaseSchema, tableId: string): Hop[] {\n const table = findTable(schema, tableId);\n return referencesFrom(schema, tableId).map((fk) => ({\n name: hopName(fk),\n fk,\n target: fk.to.table,\n optional: fk.from.columns.some((c) => table?.columns.find((col) => col.name === c)?.nullable !== false),\n }));\n}\n\n/** A table's rollup of that name where it is a latest of a reference column: the record it names can be read through it. */\nfunction latestReference(table: TableMeta, name: string): Rollup | undefined {\n const rollup = table.rollups && Object.hasOwn(table.rollups, name) ? table.rollups[name] : undefined;\n return rollup?.fn === 'latest' && rollup.references && !rollup.of ? rollup : undefined;\n}\n\nexport interface ResolvedPath {\n path: string;\n /** The foreign keys followed, in order. Empty for a plain column. */\n hops: ForeignKey[];\n hopNames: string[];\n /** The table the final column lives on. */\n table: TableMeta;\n column: ColumnMeta;\n /** Set when the path ends on a rollup of `table` rather than a column (rollups.ts). */\n rollup?: Rollup;\n /** With `rollup`: the prefix of the record the rollup is on, where that is not the path's own (a path read through the rollup, below). */\n rollupAt?: string;\n /** Set when the path ends on a computed column of `table` (computed.ts). */\n computed?: ComputedColumn;\n /** Set when the path ends on the record's tags (tagLists.ts). */\n tags?: TagSet;\n /** Set when the path reads one key of a JSON column (`specs.capacity`, definedFields.ts): the column and the key. */\n jsonKey?: { column: string; key: string };\n}\n\n/** The type of the column a path lands on, where it is that column itself: undefined for a rollup, a computed column,\n * a record's tags or a JSON key, which are expressions, and for a path that does not resolve. */\nexport function plainType(schema: DatabaseSchema, base: string, path: string): string | undefined {\n const { resolved } = tryResolvePath(schema, base, path);\n if (!resolved || resolved.rollup || resolved.computed || resolved.tags || resolved.jsonKey) return undefined;\n return resolved.column.type;\n}\n\n/**\n * Resolve a path against the schema, or explain precisely why it does not.\n *\n * Every hop is checked against the foreign key graph, so an unknown segment is\n * an error here rather than a database exception later — or, far worse, a\n * silently empty result that looks like a table with nothing in it.\n */\nexport function resolvePath(schema: DatabaseSchema, baseId: string, path: string): ResolvedPath {\n /* A path means the same against the same catalog, and a list asks for each\n of its columns' paths on every request, per row's worth of fields. Kept\n per schema object, so a reloaded App or a new catalog starts empty; a\n refused path is not kept (it throws, and is the rare one). The result is\n frozen, shared by every caller. */\n let kept = resolved.get(schema);\n if (!kept) resolved.set(schema, (kept = new Map()));\n const key = `${baseId}\\u0000${path}`;\n const hit = kept.get(key);\n if (hit) return hit;\n const made = resolvePathFresh(schema, baseId, path);\n Object.freeze(made.hops);\n Object.freeze(made.hopNames);\n // A reader can type paths (a query's `a.b.c`), and a path may loop through\n // the graph, so what is kept is bounded; the declared ones come straight back.\n if (kept.size >= MAX_KEPT_PATHS) kept.clear();\n kept.set(key, Object.freeze(made));\n return made;\n}\n\nconst resolved = new WeakMap<DatabaseSchema, Map<string, ResolvedPath>>();\nconst MAX_KEPT_PATHS = 2048;\n\nfunction resolvePathFresh(schema: DatabaseSchema, baseId: string, path: string): ResolvedPath {\n const raw = String(path ?? '').trim();\n if (!raw) throw new PathError('A path cannot be empty.', raw, '', 0);\n\n const segments = raw.split('.');\n const blank = segments.findIndex((s) => s.trim() === '');\n if (blank !== -1) {\n throw new PathError(`\"${raw}\" has an empty segment — two dots in a row, or a trailing dot.`, raw, '', blank);\n }\n\n const hopCount = segments.length - 1;\n if (hopCount > MAX_PATH_DEPTH) {\n throw new PathError(\n `\"${raw}\" follows ${hopCount} references; at most ${MAX_PATH_DEPTH} are allowed. ` +\n 'Deeper walks are usually two views rather than one.',\n raw,\n segments[MAX_PATH_DEPTH],\n MAX_PATH_DEPTH,\n );\n }\n\n let table = findTable(schema, baseId);\n if (!table) {\n throw new PathError(`No table called \"${baseId}\" in this database.`, raw, segments[0], 0);\n }\n\n const hops: ForeignKey[] = [];\n const hopNames: string[] = [];\n\n for (let i = 0; i < segments.length - 1; i++) {\n const segment = segments[i];\n const available = hopsFrom(schema, table.id);\n const matches = available.filter((h) => h.name === segment || h.fk.name === segment);\n /* The last step after a latest that reads a reference is a column of the record it names (`resident.name`): the\n rollup's key, read through that table's own relation (viewLabels.ts), so a list draws the name beside the key. */\n const through = !matches.length && i === segments.length - 2 ? latestReference(table, segment) : undefined;\n if (through) {\n const target = findTable(schema, through.references!.table);\n const wanted = segments[i + 1];\n const read = target?.columns.find((c) => c.name === wanted);\n if (!target || !read) {\n throw new PathError(`\"${wanted}\" is not a column on ${target?.name ?? through.references!.table}.`, raw, wanted, i + 1);\n }\n return {\n path: raw,\n hops,\n hopNames,\n table,\n column: read,\n rollup: { ...through, through: { table: target.id, key: through.references!.column, column: read.name } },\n rollupAt: hopNames.join('.'),\n };\n }\n /* The last step after a JSON column is one of its keys (`specs.capacity`):\n a value the definition rows name (definedFields.ts), read, filtered and\n sorted like a column. A reference of the same name wins. */\n const json =\n !matches.length && i === segments.length - 2 ? table.columns.find((c) => c.name === segment && isJsonType(c.type)) : undefined;\n if (json) {\n const key = segments[i + 1];\n if (!DEFINED_KEY.test(key))\n throw new PathError(`\"${key}\" is not a key \"${segment}\" can hold; a key is a name of letters, digits and _.`, raw, key, i + 1);\n return { path: raw, hops, hopNames, table, column: definedColumnMeta(key), jsonKey: { column: json.name, key } };\n }\n if (!matches.length) {\n /* A real column that simply is not a reference is the commonest\n mistake, and deserves its own sentence: \"total is not a reference\"\n beats \"total is not one of customer_id, sold_by\". */\n const isColumn = table.columns.some((c) => c.name === segment);\n const names = available.map((h) => h.name);\n const suggestion = nearest(segment, names);\n const hint = suggestion\n ? ` Did you mean \"${suggestion}\"?`\n : names.length\n ? ` ${table.name} references: ${shortList(names)}.`\n : ` ${table.name} has no foreign keys.`;\n throw new PathError(\n isColumn\n ? `\"${segment}\" is a column on ${table.name}, not a reference, so nothing can follow it.${hint}`\n : `\"${segment}\" is not a reference on ${table.name}.${hint}`,\n raw,\n segment,\n i,\n );\n }\n if (matches.length > 1) {\n throw new PathError(\n `\"${segment}\" is ambiguous on ${table.name} — ${matches.length} foreign keys match. ` +\n `Name the constraint instead: ${shortList(matches.map((m) => m.fk.name))}.`,\n raw,\n segment,\n i,\n );\n }\n const next = findTable(schema, matches[0].target);\n if (!next) {\n throw new PathError(`\"${segment}\" points at \"${matches[0].target}\", which is not in this schema.`, raw, segment, i);\n }\n hops.push(matches[0].fk);\n hopNames.push(matches[0].name);\n table = next;\n }\n\n const last = segments[segments.length - 1];\n if (last === LABEL_SEGMENT) {\n if (!table.labelTemplate) {\n throw new PathError(\n `${table.name} has no record label template, so \"${LABEL_SEGMENT}\" names nothing on it.`,\n raw,\n last,\n segments.length - 1,\n );\n }\n return { path: raw, hops, hopNames, table, column: labelColumnMeta() };\n }\n const column = table.columns.find((c) => c.name === last) ?? table.columns.find((c) => c.name.toLowerCase() === last.toLowerCase());\n /* A rollup is found by its exact name only, and after the columns: the\n catalog never serves one that shares a column's name, so this order\n only matters to a hand-made catalog, where the column wins. */\n const rollup = column ? undefined : table.rollups && Object.hasOwn(table.rollups, last) ? table.rollups[last] : undefined;\n if (rollup) return { path: raw, hops, hopNames, table, column: rollupColumnMeta(last, rollup), rollup };\n const computed = column ? undefined : table.computed && Object.hasOwn(table.computed, last) ? table.computed[last] : undefined;\n if (computed) return { path: raw, hops, hopNames, table, column: computedColumnMeta(last, computed), computed };\n if (!column && last === TAGS_COLUMN && table.tags)\n return { path: raw, hops, hopNames, table, column: tagsColumnMeta(), tags: table.tags };\n if (!column) {\n const suggestion = nearest(\n last,\n table.columns.map((c) => c.name),\n );\n throw new PathError(\n `\"${last}\" is not a column on ${table.name}.${suggestion ? ` Did you mean \"${suggestion}\"?` : ''}`,\n raw,\n last,\n segments.length - 1,\n );\n }\n\n return { path: raw, hops, hopNames, table, column };\n}\n\n/* ---------- many-valued relationships ---------- */\n\ninterface Backref {\n /** What to write in an aggregate's `via`. */\n name: string;\n fk: ForeignKey;\n /** The table holding the key — the \"many\" end. */\n child: string;\n}\n\n/** Every foreign key pointing at a table: the \"what refers to me\" direction. */\nexport function backrefsTo(schema: DatabaseSchema, tableId: string): Backref[] {\n return schema.foreignKeys.filter((fk) => fk.to.table === tableId).map((fk) => ({ name: fk.name, fk, child: fk.from.table }));\n}\n\ninterface ResolvedAggregate {\n aggregate: ViewAggregate;\n fk: ForeignKey;\n /** The table the children live in. */\n child: TableMeta;\n /** The table the children point at. */\n parent: TableMeta;\n /** The path prefix of the parent, '' for the base table. */\n on: string;\n column?: ColumnMeta;\n /** Default output name, e.g. `count(work_order_part)`. */\n name: string;\n}\n\nexport const AGGREGATE_FNS: AggregateFn[] = ['count', 'sum', 'min', 'max', 'avg'];\n\nexport function resolveAggregate(schema: DatabaseSchema, baseId: string, aggregate: ViewAggregate): ResolvedAggregate {\n const on = (aggregate.on ?? '').trim();\n const fail = (message: string, segment: string) => {\n throw new PathError(message, aggregate.via, segment, 0);\n };\n\n if (!AGGREGATE_FNS.includes(aggregate.fn)) {\n fail(`\"${String(aggregate.fn)}\" is not a summary. Use one of: ${AGGREGATE_FNS.join(', ')}.`, String(aggregate.fn));\n }\n\n /* The parent is the base table, or whatever a path prefix reaches. Resolved\n by resolving a path *through* it — the same code, so a prefix that is not\n a real chain of keys fails here exactly as it would anywhere else. */\n const parent = tableAtPrefix(schema, baseId, on);\n if (!parent) {\n fail(on ? `\"${on}\" is not a chain of references from ${baseId}.` : `No table called \"${baseId}\" in this database.`, on || baseId);\n }\n\n const candidates = backrefsTo(schema, parent!.id);\n // A qualified relationship spelling identifies endpoints, not a constraint.\n // Prefer it when a physical constraint happens to have the same text; App\n // logical-to-physical transfers use this form to preserve the selected edge.\n const match =\n candidates.find((b) => `${b.child}.${b.fk.from.columns.join('+')}` === aggregate.via) ??\n candidates.find((b) => b.name === aggregate.via);\n if (!match) {\n fail(\n `Nothing called \"${aggregate.via}\" points at ${parent!.name}.` +\n (candidates.length ? ` It is referred to by: ${shortList(candidates.map((c) => c.name))}.` : ' Nothing refers to it.'),\n aggregate.via,\n );\n }\n\n const child = findTable(schema, match!.child);\n if (!child) fail(`\"${match!.child}\" is not in this schema.`, aggregate.via);\n\n let column: ColumnMeta | undefined;\n if (aggregate.fn !== 'count') {\n if (!aggregate.column) {\n fail(`${aggregate.fn} needs a column on ${child!.name} to summarise.`, aggregate.via);\n }\n column = child!.columns.find((c) => c.name === aggregate.column);\n if (!column) {\n const suggestion = nearest(\n aggregate.column!,\n child!.columns.map((c) => c.name),\n );\n fail(\n `\"${aggregate.column}\" is not a column on ${child!.name}.${suggestion ? ` Did you mean \"${suggestion}\"?` : ''}`,\n aggregate.column!,\n );\n }\n }\n\n const subject = `${on ? `${on}.` : ''}${child!.name}${column ? `.${column.name}` : ''}`;\n return {\n aggregate,\n fk: match!.fk,\n child: child!,\n parent: parent!,\n on,\n column,\n name: aggregate.alias?.trim() || `${aggregate.fn}(${subject})`,\n };\n}\n\n/** The table a chain of hop names lands on. Empty prefix is the base itself. */\nexport function tableAtPrefix(schema: DatabaseSchema, baseId: string, prefix: string): TableMeta | undefined {\n let table = findTable(schema, baseId);\n if (!prefix) return table;\n for (const segment of prefix.split('.')) {\n if (!table) return undefined;\n const hop = hopsFrom(schema, table.id).find((h) => h.name === segment || h.fk.name === segment);\n if (!hop) return undefined;\n table = findTable(schema, hop.target);\n }\n return table;\n}\n\n/** Resolve without throwing — for callers collecting several errors at once. */\nexport function tryResolvePath(schema: DatabaseSchema, baseId: string, path: string): { resolved?: ResolvedPath; error?: PathError } {\n try {\n return { resolved: resolvePath(schema, baseId, path) };\n } catch (err) {\n if (err instanceof PathError) return { error: err };\n throw err;\n }\n}\n","/*\n * Row scopes: the per-resource row filters a server-owned policy supplies,\n * captured within fixed limits, and the scoped relations a read or write is\n * compiled against. Shared so views and breakdowns compile under a scope\n * through the same builder as a plain read.\n */\nimport { buildWhere, OPS, type Filter, type SqlFragment, type SqlStyle } from './filter.js';\nimport { Refusal } from './refusal.js';\nimport { findTable, type DatabaseSchema } from './schema.js';\nimport { isFixedScopeCatalog } from './scopeCatalog.js';\nimport { qualifiedTable } from './sqlRelation.js';\nimport { plainType, tryResolvePath } from './viewPaths.js';\n\n/** Internal, server-owned compiler input; never a member of a request definition.\n * Exact catalog IDs only. With a scope present, missing resources deny rows.\n * `true` explicitly allows all rows; filters address local columns, not paths.\n * This primitive alone does not enable App authorization or tenant isolation.\n */\nexport interface RowScopeFilter extends Filter {\n /** Private SQLite comparison metadata, never SQL or a request filter field.\n * Applies BINARY collation to these local columns; LIKE keeps its own semantics. */\n readonly binary?: readonly string[];\n}\nexport type RowScope = Readonly<Record<string, boolean | RowScopeFilter>>;\n\n/** Composition must budget expanded data against the same capture limits. */\nexport const ROW_SCOPE_LIMITS = Object.freeze({\n resources: 256,\n conditions: 2048,\n array: 4096,\n nodes: 16_384,\n string: 16_384,\n characters: 262_144,\n});\n\nconst invalid = (): never => {\n throw new Refusal('Invalid row scope.');\n};\n\n/** Copy before validation so accessors, prototypes and later mutation cannot\n * change what was checked. Bounds apply to the entire scope, not each table.\n * Frozen nested values stay private; Readonly<Record> alone is not a snapshot.\n */\nexport function captureRowScope(input: RowScope): RowScope {\n let nodes = 0;\n let characters = 0;\n const copy = (value: unknown, depth = 0): unknown => {\n if (++nodes > ROW_SCOPE_LIMITS.nodes || depth > 8) return invalid();\n if (typeof value === 'string') {\n characters += value.length;\n if (value.length > ROW_SCOPE_LIMITS.string || characters > ROW_SCOPE_LIMITS.characters) return invalid();\n return value;\n }\n if (typeof value === 'boolean' || (typeof value === 'number' && Number.isFinite(value))) return value;\n if (!value || typeof value !== 'object') return invalid();\n const array = Array.isArray(value);\n const proto = Object.getPrototypeOf(value);\n if (array ? proto !== Array.prototype : proto !== Object.prototype && proto !== null) return invalid();\n const keys = Reflect.ownKeys(value);\n if (keys.length > ROW_SCOPE_LIMITS.array + 1) return invalid();\n if (array) {\n const length = Object.getOwnPropertyDescriptor(value, 'length')?.value;\n if (!Number.isInteger(length) || length > ROW_SCOPE_LIMITS.array || keys.length !== length + 1) return invalid();\n const result: unknown[] = [];\n for (let i = 0; i < length; i++) {\n const descriptor = Object.getOwnPropertyDescriptor(value, String(i));\n if (!descriptor || !('value' in descriptor) || !descriptor.enumerable) return invalid();\n result.push(copy(descriptor.value, depth + 1));\n }\n return Object.freeze(result);\n }\n const result: Record<string, unknown> = Object.create(null);\n for (const key of keys) {\n if (typeof key !== 'string') return invalid();\n characters += key.length;\n if (key.length > ROW_SCOPE_LIMITS.string || characters > ROW_SCOPE_LIMITS.characters) return invalid();\n const descriptor = Object.getOwnPropertyDescriptor(value, key)!;\n if (!('value' in descriptor) || !descriptor.enumerable) return invalid();\n result[key] = copy(descriptor.value, depth + 1);\n }\n return Object.freeze(result);\n };\n const scope = copy(input);\n if (!scope || typeof scope !== 'object' || Array.isArray(scope) || Object.keys(scope).length > ROW_SCOPE_LIMITS.resources)\n return invalid();\n return scope as RowScope;\n}\n\n/* The scopes this module copied, froze and checked, each with the catalogs it\n was checked against. A tenant read snapshotted one request's scope again at\n every layer it crossed — composed, authorized, bound, and once more per\n statement — each a full copy and a scan of the catalog for every resource:\n a field report's profile put it among the largest costs of a page. A\n snapshot is frozen all the way down and was made here, never by a caller,\n so checking it again against the same catalog object could only pass and\n copy it to an equal value: it is handed back as it is. A catalog that can\n still change is checked afresh every time, as before; anything a caller\n built, however frozen, is copied every time and then found by its text\n (below), never handed back as itself. */\nconst snapshots = new WeakMap<object, WeakSet<DatabaseSchema>>();\nconst checkedAgainst = (scope: object, schema: DatabaseSchema) => snapshots.get(scope)?.has(schema) === true;\n\n/* And by what they say. A policy that names its reader (`owner_account_id = <the account>`) builds its scope afresh\n on every request: a new object each time, equal to the last, so every request copied and checked it again, and\n every cache keyed by a scope's identity downstream (tenantReadScope.ts compositions, the scoped reads' kept\n statements) missed, composing and compiling afresh too — a field report's tenant App, whose staff scope names\n two hundred tables and the reader, spent a sixth of a tenant enter there. The copy is still made, so nothing the\n caller holds can change what is checked; it is then looked up by its text against a catalog nothing can change,\n and an equal scope already checked against it is handed back. The text is the copy's own — plain strings,\n booleans, finite numbers, arrays and records, nothing else survives captureRowScope — so equal text is an equal\n scope. Bounded per catalog; a reader past the bound is checked as before. */\nconst byText = new WeakMap<DatabaseSchema, Map<string, RowScope>>();\nconst BY_TEXT_HELD = 256;\n\n/** A scope's text, where the text says all of it: undefined for anything JSON would say the same as something else\n * (an undefined value, which it drops; a non-finite number; an object that is not a plain record). Only for data\n * nothing can intercept — a copy this module or the tenant capture made — never a caller's live object, whose\n * accessors JSON.stringify would run. */\nexport function rowScopeText(value: unknown): string | undefined {\n let plain = true;\n const text = JSON.stringify(value, (_key, item: unknown) => {\n if (\n item === undefined ||\n (typeof item === 'number' && !Number.isFinite(item)) ||\n (item !== null && typeof item === 'object' && !Array.isArray(item) && ![null, Object.prototype].includes(Object.getPrototypeOf(item)))\n )\n plain = false;\n return item;\n });\n return plain ? text : undefined;\n}\n\n/** Test-only: how many row scopes were copied and checked in full rather than handed back. */\nexport const rowScopeCounters = { checked: 0 };\n\n/** Whether `value` is a row scope snapshotRowScope made: plain data, frozen all the way down, that nothing can change. */\nexport function isRowScopeSnapshot(value: unknown): boolean {\n return value !== null && typeof value === 'object' && snapshots.has(value);\n}\n\n/** Capture can precede an async catalog load; catalog validation cannot. */\nexport function snapshotRowScope(schema: DatabaseSchema, input: RowScope): RowScope {\n if (input !== null && typeof input === 'object' && checkedAgainst(input, schema)) return input;\n const record = (value: unknown): value is Record<string, unknown> => !!value && typeof value === 'object' && !Array.isArray(value);\n /* A snapshot checked against another catalog — the same policy's constant scope under a second deployment of the\n App in this process, after a reload — is already plain frozen data this module made, so it is checked as it is\n and kept against this catalog too. Copied, its copy was a new object each time, which the caches keyed by a\n scope (tenantReadScope.ts) never found again: every request after a reload checked and composed afresh. */\n const scope = isRowScopeSnapshot(input) ? input : captureRowScope(input);\n const fixed = isFixedScopeCatalog(schema);\n const text = fixed && scope !== input ? rowScopeText(scope) : undefined;\n const held = text === undefined ? undefined : byText.get(schema);\n const known = text === undefined ? undefined : held?.get(text);\n if (known) return known;\n rowScopeCounters.checked++;\n let conditions = 0;\n for (const [id, policy] of Object.entries(scope)) {\n const table = findTable(schema, id);\n if (!table) return invalid();\n if (typeof policy === 'boolean') continue;\n if (\n !record(policy) ||\n Object.keys(policy).some((key) => key !== 'groups' && key !== 'binary') ||\n !Array.isArray(policy.groups) ||\n !policy.groups.length\n )\n return invalid();\n if (Object.hasOwn(policy, 'binary')) {\n if (\n schema.dialect !== 'sqlite' ||\n !Array.isArray(policy.binary) ||\n policy.binary.some((name) => typeof name !== 'string' || !table.columns.some((column) => column.name === name)) ||\n new Set(policy.binary).size !== policy.binary.length\n )\n return invalid();\n }\n for (const group of policy.groups) {\n if (!Array.isArray(group) || !group.length) return invalid();\n for (const condition of group) {\n if (++conditions > ROW_SCOPE_LIMITS.conditions || !record(condition)) return invalid();\n if (Object.keys(condition).some((key) => !['column', 'op', 'value'].includes(key))) return invalid();\n if (typeof condition.column !== 'string' || !table.columns.some((column) => column.name === condition.column)) return invalid();\n if (typeof condition.op !== 'string' || !OPS.has(condition.op)) return invalid();\n if (condition.op === 'isNull' || condition.op === 'isNotNull') {\n if (Object.hasOwn(condition, 'value')) return invalid();\n } else if (condition.op === 'in') {\n if (!Array.isArray(condition.value) || condition.value.some((value) => typeof value !== 'string' && typeof value !== 'number'))\n return invalid();\n } else if (['contains', 'startsWith', 'endsWith', 'like'].includes(condition.op)) {\n if (typeof condition.value !== 'string') return invalid();\n } else if (!['string', 'number', 'boolean'].includes(typeof condition.value)) return invalid();\n }\n }\n }\n // Kept only against a catalog nothing can change; checked against one that can, it is checked again next time.\n if (fixed) {\n const against = snapshots.get(scope);\n if (against) against.add(schema);\n else snapshots.set(scope, new WeakSet([schema]));\n if (text !== undefined) {\n let kept = held;\n if (!kept) byText.set(schema, (kept = new Map()));\n if (kept.size >= BY_TEXT_HELD) kept.delete(kept.keys().next().value!);\n kept.set(text, scope);\n }\n }\n return scope as RowScope;\n}\n\n/**\n * A scope with each group's repeated conditions said once. Composed scopes\n * repeat themselves — a tenant's ownership condition and a membership rule\n * that names the same column and value — and `\"tenant_id\" = $1 AND\n * \"tenant_id\" = $2` is the same rows as either half, while the planner\n * estimates it as two independent predicates and misjudges every join above\n * it. Only a condition identical in every field is dropped:\n * a conjunction of one condition with itself is that condition, so no row\n * the scope admits or refuses changes.\n */\nfunction withoutRepeats(policy: RowScopeFilter): Filter {\n return {\n groups: policy.groups.map((group) => {\n const seen = new Set<string>();\n return group.filter((condition) => {\n // Every field, in a fixed order: a collation or a column comparison is a different condition.\n const key = JSON.stringify(\n Object.keys(condition)\n .sort()\n .map((name) => [name, (condition as unknown as Record<string, unknown>)[name]]),\n );\n if (seen.has(key)) return false;\n seen.add(key);\n return true;\n });\n }),\n };\n}\n\n/* A catalog's table names, lower-cased: what a scoped relation's name must not\n shadow. The same for every statement compiled over a catalog nothing can\n change, so it is listed once for such a catalog, not once per statement. */\nconst reservedOf = new WeakMap<DatabaseSchema, readonly string[]>();\nfunction reservedNames(schema: DatabaseSchema): readonly string[] {\n const kept = reservedOf.get(schema);\n if (kept) return kept;\n const names = schema.tables.map((table) => table.name.toLowerCase());\n if (isFixedScopeCatalog(schema)) reservedOf.set(schema, names);\n return names;\n}\n\n/** Every occurrence of a resource reads the same filtered relation, including\n * LEFT JOIN targets and correlated aggregate children. CTE bindings precede\n * outer filters, independent of SQL SELECT/FROM expression order. Call finish\n * only after all relations have been discovered, then number outer bindings.\n */\nexport class ScopedRelations {\n private readonly scope: RowScope | undefined;\n private readonly names = new Map<string, string>();\n private readonly reserved: Set<string>;\n private readonly definitions: string[] = [];\n private readonly params: unknown[] = [];\n private finished = false;\n private ordinal = 0;\n\n constructor(\n private schema: DatabaseSchema,\n private style: SqlStyle,\n scope?: RowScope,\n ) {\n this.scope = scope === undefined ? undefined : snapshotRowScope(schema, scope);\n this.reserved = new Set(reservedNames(schema));\n }\n\n table = (id: string): string => {\n if (this.scope === undefined) return qualifiedTable(this.schema, id, this.style);\n const existing = this.names.get(id);\n if (existing) return this.style.quote(existing);\n if (this.finished) throw new Error('Row scope relations must be planned before outer parameters.');\n const table = findTable(this.schema, id);\n if (!table || !table.columns.length) return invalid();\n // '$' cannot occur in JoinPlan's sanitized aliases; catalog names still\n // need a case-insensitive collision check to avoid CTE self-shadowing.\n let name: string;\n do {\n name = `_tw_scope$${++this.ordinal}`;\n } while (this.reserved.has(name.toLowerCase()));\n this.reserved.add(name.toLowerCase());\n this.names.set(id, name);\n const policy = this.scope[id] ?? false;\n const where =\n typeof policy === 'boolean'\n ? { text: policy ? '' : ` WHERE ${this.style.never ?? '1 = 0'}`, params: [] }\n : buildWhere(\n withoutRepeats(policy),\n this.style,\n this.params.length + 1,\n (column) => this.style.quote(column) + (policy.binary?.includes(column) ? ' COLLATE BINARY' : ''),\n undefined,\n undefined,\n undefined,\n false,\n (column) => table.columns.find((c) => c.name === column)?.type,\n );\n const columns = table.columns.map((column) => this.style.quote(column.name)).join(', ');\n // The style's hint (cteHint) goes between AS and the body: Postgres inlines the CTE, so the\n // scope's predicate meets the read's own filter, sort and limit instead of a copied slice.\n this.definitions.push(\n `${this.style.quote(name)} AS${this.style.cteHint ?? ''} (SELECT ${columns} FROM ${qualifiedTable(this.schema, id, this.style)}${where.text})`,\n );\n this.params.push(...where.params);\n return this.style.quote(name);\n };\n\n private readonly narrowed = new Map<string, string>();\n\n /**\n * A table's rows narrowed by an authored filter over its own columns — the\n * rows a rollup counts (rollups.ts) — as one more definition here. A CTE\n * rather than a WHERE inside the correlated subquery, for the parameters:\n * a subquery can sit in the SELECT list, the WHERE and the ORDER BY at\n * once, and `?` placeholders bind by position, so values written there\n * would interleave with the outer filter's. Here they bind first, with the\n * scope's, and the subquery names the relation with no values of its own.\n * Read from the scoped relation where there is a scope, so the filter\n * narrows within the reader's rows, never beside them.\n */\n filtered = (id: string, filter: Filter, written?: Readonly<Record<string, (own: (column: string) => string) => string>>): string => {\n const key = `${id}\\u0000${JSON.stringify(filter)}\\u0000${Object.keys(written ?? {}).join(',')}`;\n const existing = this.narrowed.get(key);\n if (existing) return this.style.quote(existing);\n if (this.finished) throw new Error('Filtered relations must be planned before outer parameters.');\n const source = this.table(id);\n const table = findTable(this.schema, id);\n if (!table || !table.columns.length) return invalid();\n let name: string;\n do {\n name = `_tw_scope$${++this.ordinal}`;\n } while (this.reserved.has(name.toLowerCase()));\n this.reserved.add(name.toLowerCase());\n this.narrowed.set(key, name);\n const q = this.style.quote;\n /* A path (`sales_order_id.status`) is the column it lands on, read by one scalar subquery that walks\n its references from this row through the statement's relations — so a row policy on the order\n narrows what the path can see, as it does a list's join. Planned before the WHERE is written, for\n the parameters: a relation's own scope binds ahead of this filter's values. */\n const named = filter.groups.flatMap((group) => group.flatMap((c) => [c.column, ...(c.other === undefined ? [] : [c.other])]));\n const row = named.some((column) => column.includes('.')) ? '_tw_row' : undefined;\n const own = (column: string) => (row ? `${q(row)}.${q(column)}` : q(column));\n const paths = new Map<string, string>();\n for (const path of named) {\n // A computed column of the row (a rollup's query, rollups.ts `computed`): its expression over this row's columns.\n if (written && Object.hasOwn(written, path)) {\n if (!paths.has(path)) paths.set(path, `(${written[path](own)})`);\n continue;\n }\n if (!path.includes('.') || paths.has(path)) continue;\n const { resolved } = tryResolvePath(this.schema, id, path);\n if (!resolved || !resolved.hops.length || resolved.rollup || resolved.computed || resolved.tags || resolved.jsonKey) return invalid();\n const aliases = resolved.hops.map(() => `_tw_hop$${++this.ordinal}`);\n const joins = resolved.hops.map((hop, at) => {\n const on = hop.to.columns\n .map(\n (to, i) => `${q(aliases[at])}.${q(to)} = ${at ? `${q(aliases[at - 1])}.${q(hop.from.columns[i])}` : own(hop.from.columns[i])}`,\n )\n .join(' AND ');\n return { relation: `${this.table(hop.to.table)} ${q(aliases[at])}`, on };\n });\n const [first, ...rest] = joins;\n paths.set(\n path,\n `(SELECT ${q(aliases[aliases.length - 1])}.${q(resolved.column.name)} FROM ${first.relation}` +\n rest.map((join) => ` JOIN ${join.relation} ON ${join.on}`).join('') +\n ` WHERE ${first.on})`,\n );\n }\n const local = (column: string) => paths.get(column) ?? own(column);\n // A column's type, or a path's where it lands on a column: a computed one is an expression, compared as written.\n const typeOf = (column: string) => {\n if (written && Object.hasOwn(written, column)) return undefined;\n if (!column.includes('.')) return table.columns.find((c) => c.name === column)?.type;\n return plainType(this.schema, id, column);\n };\n const where = buildWhere(filter, this.style, this.params.length + 1, local, local, undefined, undefined, false, typeOf);\n const columns = table.columns.map((column) => q(column.name)).join(', ');\n this.definitions.push(`${q(name)} AS (SELECT ${columns} FROM ${source}${row ? ` ${q(row)}` : ''}${where.text})`);\n this.params.push(...where.params);\n return this.style.quote(name);\n };\n\n finish(): SqlFragment {\n this.finished = true;\n return { text: this.definitions.length ? `WITH ${this.definitions.join(',\\n ')}\\n` : '', params: [...this.params] };\n }\n}\n","/**\n * Multi-table views, built out of **reference paths**.\n *\n * The `Filter` in filter.ts addresses exactly one table and must keep doing\n * so: it is the shape every read goes through, and widening it to carry joins\n * would put join-building into every code path that filters anything. So the\n * multi-table story is a separate idea rather than a bigger filter, and the\n * idea is dot-walking:\n *\n * invoice customer_id.name contains Harbour\n * work_order assigned_to.last_name = Petrova\n * and customer_id.country_code.name = Australia\n *\n * A path is a chain of foreign keys starting at the base table, ending at a\n * column. `customer_id.name` means \"follow invoice.customer_id to customer,\n * then take name\". Nothing about it is guessed: the foreign key graph is\n * already introspected, so every hop is derivable, checkable, and reversible\n * into SQL — a path compiles to a LEFT JOIN chain and a qualified column.\n *\n * One mechanism answers three separate questions. Filtering across a long\n * walk, showing a field from a related table, and the composer UI are all the\n * same thing seen from three angles; the composer is a visual editor for the\n * path set.\n *\n * Two properties are load-bearing and are pinned by tests:\n *\n * - **LEFT JOIN, never INNER.** A null foreign key must leave the base row\n * in the result with nulls beside it. A composer that quietly drops rows\n * is worse than no composer at all, because the number it shows you is\n * wrong in a direction you cannot see.\n * - **Every value is a bound parameter.** Only identifiers are ever quoted\n * into the text, and they are quoted by the dialect's `SqlStyle`. The\n * WHERE clause is still built by `buildWhere`, which now takes an\n * identifier resolver so a path can become `\"customer_1\".\"name\"` without\n * a second predicate builder existing to disagree with the first.\n */\nimport {\n ANSI_STYLE,\n buildWhere,\n collate,\n orderClauseSql,\n type Condition,\n type Filter,\n type Op,\n type SqlFragment,\n type SqlStyle,\n} from './filter.js';\nimport { clampLimit, clampOffset, countStatement } from './reads.js';\nimport {\n aggregateInput,\n declaredOrder,\n findTable,\n type ColumnMeta,\n type DatabaseSchema,\n type ForeignKey,\n type TableMeta,\n} from './schema.js';\nimport { ANY_CONDITION, anyConditions, expandSegments, parseQuery, splitClauseTail, type QueryError, type QueryPerson } from './query.js';\nimport { stitchedListProblem } from './stitchedPaths.js';\nimport { qualifiedTable } from './sqlRelation.js';\nimport { ScopedRelations, type RowScope } from './rowScope.js';\nimport { slug } from './slug.js';\nimport { maskQuoted } from './maskQuoted.js';\nimport {\n type ViewColumn,\n type ViewCondition,\n type ViewFilter,\n type ViewOrder,\n type AggregateFn,\n type ViewAggregate,\n type ViewDef,\n PathError,\n type ResolvedPath,\n AGGREGATE_FNS,\n resolveAggregate,\n tryResolvePath,\n labelColumnMeta,\n backrefsTo,\n} from './viewPaths.js';\nimport { viewPaths, withViewComputed, withViewLabels } from './viewLabels.js';\nimport { rollupColumnMeta } from './rollups.js';\nimport { TAGS_COLUMN, tagsColumnMeta } from './tagLists.js';\nimport { computedColumnMeta } from './computed.js';\nimport { withViewReferences } from './declaredReferences.js';\nexport { withViewComputed, withViewLabels } from './viewLabels.js';\nexport {\n type ViewColumn,\n type ViewFilter,\n type ViewOrder,\n type AggregateFn,\n type ViewAggregate,\n type ViewDef,\n MAX_PATH_DEPTH,\n PathError,\n hopName,\n type Hop,\n hopsFrom,\n type ResolvedPath,\n resolvePath,\n backrefsTo,\n resolveAggregate,\n tableAtPrefix,\n tryResolvePath,\n} from './viewPaths.js';\n\n/* ---------- compilation ---------- */\n\n/* A view's filter as a column filter, for `buildWhere`. A condition's\n collation travels with it: dropped here, a keyset comparison would fall\n back to the column's collation while its ORDER BY kept the binary one. */\nfunction viewConditions(view: ViewDef): Filter | undefined {\n return view.filter?.groups.length\n ? {\n groups: view.filter.groups.map((group) =>\n group.map((c): Condition => ({\n column: c.path,\n op: c.op,\n value: c.value,\n ...(c.collation === undefined ? {} : { collation: c.collation }),\n ...(c.other === undefined ? {} : { other: c.other }),\n ...(c.any === undefined ? {} : { any: c.any }),\n })),\n ),\n }\n : undefined;\n}\n\nexport interface CompiledColumn {\n /** The path this column came from. */\n path: string;\n /** The output name in the result set. */\n name: string;\n /** Table the value actually lives on, for the UI's grouping. */\n table: string;\n column: string;\n type: string;\n nullable: boolean;\n primaryKey: boolean;\n /** How many references were followed to reach it. 0 is the base table. */\n hops: number;\n /** Set when the column is a summary of a many-valued relationship; `latest` is a rollup's newest row (rollups.ts). */\n aggregate?: AggregateFn | 'latest';\n}\n\nexport interface CompiledJoin {\n alias: string;\n table: string;\n /** The path prefix this join corresponds to, e.g. `customer_id.country_code`. */\n path: string;\n}\n\ninterface CompiledView {\n text: string;\n params: unknown[];\n columns: CompiledColumn[];\n joins: CompiledJoin[];\n limit: number;\n}\n\n/**\n * Alias generation.\n *\n * Two requirements pull in opposite directions. Aliases must never collide —\n * `work_order` in the demo has three separate references to `employee`, so a\n * scheme keyed on the target table alone produces three joins all called\n * `employee` and a statement that does not parse. And they must be stable, so\n * the same view compiles to the same SQL every time and a diff of the explain\n * panel means something.\n *\n * Both fall out of keying on the *path prefix* rather than the table: a prefix\n * identifies a join uniquely, so the same path used twice reuses one join and\n * two paths through the same table get two. The readable table name is kept as\n * a prefix and made unique by an ordinal assigned in first-use order.\n */\nexport class JoinPlan {\n readonly joins: CompiledJoin[] = [];\n private byPrefix = new Map<string, string>();\n private n = 0;\n\n constructor(\n private schema: DatabaseSchema,\n base: TableMeta,\n private style: SqlStyle,\n private relation: (id: string) => string = (id) => qualifiedTable(schema, id, style),\n ) {\n this.byPrefix.set('', alias(base.name, 0));\n }\n\n get baseAlias(): string {\n return this.byPrefix.get('')!;\n }\n\n /** The alias for a path prefix, creating the join chain if it is new. */\n aliasFor(hops: ForeignKey[], hopNames: string[]): string {\n let prefix = '';\n for (let i = 0; i < hops.length; i++) {\n prefix = prefix ? `${prefix}.${hopNames[i]}` : hopNames[i];\n /* Already planned: the same path used twice must reuse one join, not\n add a second copy of the same table under a different alias. */\n if (this.byPrefix.has(prefix)) continue;\n const target = findTable(this.schema, hops[i].to.table)!;\n this.byPrefix.set(prefix, alias(target.name, ++this.n));\n this.joins.push({ alias: this.byPrefix.get(prefix)!, table: target.id, path: prefix });\n }\n return this.byPrefix.get(prefix) ?? this.baseAlias;\n }\n\n /** The fully qualified, quoted SQL for a resolved path's final column. */\n qualify(resolved: ResolvedPath): string {\n const owner = this.aliasFor(resolved.hops, resolved.hopNames);\n return `${this.style.quote(owner)}.${this.style.quote(resolved.column.name)}`;\n }\n\n /** The relation a table is read as in this statement: its scoped CTE, or the table itself. */\n relationOf(id: string): string {\n return this.relation(id);\n }\n\n /** The alias of a path prefix that has already been planned, or the base. */\n aliasOfPrefix(prefix: string): string {\n return this.byPrefix.get(prefix) ?? this.baseAlias;\n }\n\n /**\n * A fresh alias for something that is not a join — a subquery's table.\n *\n * It draws from the same counter as the joins, which is what guarantees a\n * correlated subquery over `employee` cannot pick the alias a LEFT JOIN\n * over `employee` is already using. Two `employee_3`s in one statement is\n * a self-join nobody asked for, and the wrong answer rather than an error.\n */\n subAlias(name: string): string {\n return alias(name, ++this.n);\n }\n\n /**\n * The JOIN clauses, in the order the aliases were created.\n *\n * Order matters for more than tidiness: a join may only reference an alias\n * introduced before it, and creation order is exactly path order, so a\n * nested hop always follows the hop it hangs off.\n */\n clauses(): string[] {\n return this.joins.map((join) => {\n const parts = join.path.split('.');\n const parentPrefix = parts.slice(0, -1).join('.');\n const parent = this.byPrefix.get(parentPrefix)!;\n const fk = this.fkFor(join.path)!;\n const target = findTable(this.schema, join.table)!;\n /* Every column of the key, not just the first. A composite foreign key\n joined on one column is a cross product wearing a join's clothes. */\n const on = fk.to.columns\n .map(\n (toCol, i) =>\n `${this.style.quote(join.alias)}.${this.style.quote(toCol)} = ` +\n `${this.style.quote(parent)}.${this.style.quote(fk.from.columns[i] ?? fk.from.columns[0])}`,\n )\n .join(' AND ');\n /* LEFT, always. An INNER JOIN here would drop every base row whose\n foreign key is null — silently, and in a direction that looks like\n the data simply is not there. */\n return ` LEFT JOIN ${this.relation(target.id)} ${this.style.quote(join.alias)} ON ${on}`;\n });\n }\n\n private fkChain = new Map<string, ForeignKey>();\n\n /** Remember which key produced each join, so `clauses` can rebuild the ON. */\n record(path: string, fk: ForeignKey): void {\n if (!this.fkChain.has(path)) this.fkChain.set(path, fk);\n }\n\n private fkFor(path: string): ForeignKey | undefined {\n return this.fkChain.get(path);\n }\n}\n\n/** Alias text: the table's own name, made unique by an ordinal. */\nfunction alias(name: string, n: number): string {\n /* Truncated because Postgres silently cuts identifiers at 63 bytes, and a\n silent cut is exactly how two long table names become one alias. */\n const safe = name.replace(/[^A-Za-z0-9_]/g, '_').slice(0, 40) || 't';\n return `${safe}_${n}`;\n}\n\n/**\n * Compile a view to one parameterised SELECT.\n *\n * Order of work is deliberate and is what makes the output deterministic:\n * columns, then the filter, then the sort. Aliases are handed out on first\n * use, so the same view always produces the same statement.\n */\nexport function compileView(\n schema: DatabaseSchema,\n view: ViewDef,\n style: SqlStyle,\n offset: number | unknown = 0,\n scope?: RowScope,\n): CompiledView {\n // The App's soft keys, where this catalog is the database's own (declaredReferences.ts).\n schema = withViewReferences(schema, view.references);\n const base = findTable(schema, view.base);\n if (!base) {\n throw new PathError(`No table called \"${view.base}\" in this database, so the view has nothing to start from.`, view.base, view.base, 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\n const { resolve, qualify, membership, exists, typeOf } = viewPaths(schema, view, plan, style, relations);\n\n /* An empty column list means the base table's own columns. Expanding it\n here rather than emitting `SELECT *` keeps the result's column list\n knowable without running the statement, which is what the composer's\n field checklist reads. */\n const wanted: ViewColumn[] = view.columns?.length ? view.columns : base.columns.map((c) => ({ path: c.name }));\n\n const seen = new Map<string, number>();\n const columns: CompiledColumn[] = [];\n const select: string[] = [];\n\n for (const col of wanted) {\n const resolved = resolve(col.path);\n const sql = qualify(col.path);\n /* Two paths can legitimately end on the same name — `customer_id.name`\n and `sold_by.name` are both \"name\" if aliased carelessly. The path is\n already unique, so it is the default; an explicit collision gets a\n numeric suffix rather than one column quietly overwriting the other. */\n let name = col.alias?.trim() || resolved.path;\n const count = seen.get(name) ?? 0;\n seen.set(name, count + 1);\n if (count) name = `${name}_${count + 1}`;\n\n columns.push({\n path: resolved.path,\n name,\n table: resolved.table.id,\n column: resolved.column.name,\n type: resolved.column.type,\n /* A column on a joined table is nullable in the result even when the\n column itself is NOT NULL: the LEFT JOIN can produce no row at all.\n Saying otherwise would have the UI render a real null as an error. */\n nullable: resolved.column.nullable || resolved.hops.length > 0,\n primaryKey: resolved.column.primaryKey,\n hops: resolved.hops.length,\n // A rollup reads like a column and is a summary: nothing edits it.\n ...(resolved.rollup && !resolved.rollup.through ? { aggregate: resolved.rollup.fn } : {}),\n });\n select.push(`${sql} AS ${style.quote(name)}`);\n }\n\n /* Aggregates come after the plain columns, and after them on purpose: a\n summary hanging off a joined table needs that join's alias to exist, and\n resolving columns first is what puts it there. */\n for (const aggregate of view.aggregates ?? []) {\n const resolved = resolveAggregate(schema, view.base, aggregate);\n /* The parent side is a path prefix, so an aggregate can hang off a joined\n table as easily as off the base — \"invoice lines on the invoice this\n shipment belongs to\" is the same construction one hop along. */\n if (resolved.on) {\n /* Qualifying any column on the parent is what plans the joins leading\n to it and records their keys. Doing it through the same `qualify`\n the columns use is deliberate: two ways of planning a join is two\n ways for the aliases to disagree. */\n qualify(`${resolved.on}.${resolved.parent.columns[0].name}`);\n }\n const parentAlias = resolved.on ? plan.aliasOfPrefix(resolved.on) : plan.baseAlias;\n\n const childAlias = plan.subAlias(resolved.child.name);\n const on = resolved.fk.to.columns\n .map(\n (toCol, i) =>\n `${style.quote(childAlias)}.${style.quote(resolved.fk.from.columns[i] ?? resolved.fk.from.columns[0])} = ` +\n `${style.quote(parentAlias)}.${style.quote(toCol)}`,\n )\n .join(' AND ');\n\n /* COUNT(*) rather than COUNT(column): a column count skips nulls, which\n turns \"how many parts\" into \"how many parts have a quantity\" without\n saying so. SUM over no rows is left as NULL rather than coerced to 0 —\n a work order with no parts has no total, and 0 is a claim about money\n that nobody made. */\n const expression =\n resolved.aggregate.fn === 'count'\n ? 'COUNT(*)'\n : `${resolved.aggregate.fn.toUpperCase()}(${aggregateInput(\n `${style.quote(childAlias)}.${style.quote(resolved.column!.name)}`,\n resolved.column!.type,\n style.dialect,\n )})`;\n\n let name = resolved.name;\n const count = seen.get(name) ?? 0;\n seen.set(name, count + 1);\n if (count) name = `${name}_${count + 1}`;\n\n select.push(\n `(SELECT ${expression} FROM ${relations.table(resolved.child.id)} ${style.quote(childAlias)}` +\n ` WHERE ${on}) AS ${style.quote(name)}`,\n );\n columns.push({\n path: resolved.name,\n name,\n table: resolved.child.id,\n column: resolved.column?.name ?? '*',\n type: resolved.aggregate.fn === 'count' ? 'integer' : (resolved.column?.type ?? 'integer'),\n // COUNT is never null; every other summary is, over an empty child set.\n nullable: resolved.aggregate.fn !== 'count',\n primaryKey: false,\n hops: resolved.on ? resolved.on.split('.').length : 0,\n aggregate: resolved.aggregate.fn,\n });\n }\n\n /* The filter is compiled by `buildWhere`, not beside it. One predicate\n builder means one place where a value could stop being a parameter. */\n const conditions = viewConditions(view);\n // Preserve first-use alias ordering, but discover filter and sort joins\n // before freezing the scope prefix and numbering ordinary parameters.\n for (const group of conditions?.groups ?? []) {\n for (const condition of group) {\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 const aliasNames = new Set((view.aggregates ?? []).map((a) => a.alias?.trim()).filter(Boolean));\n const order = (view.orderBy ?? []).flatMap((o) => {\n if (o.nulls === undefined) return [orderTerm(o)];\n if (o.nulls !== 'last' || o.alias || o.near !== undefined) {\n throw new PathError(`\"${o.path}\" puts nulls last only when it is a column.`, o.path, o.path, 0);\n }\n /* NULLS LAST in both directions, said the one way every dialect reads:\n a null sorts after every value whichever way the column runs. */\n return [`${qualify(o.path)} IS NULL ASC`, orderTerm(o)];\n });\n function orderTerm(o: ViewOrder) {\n if (o.collation !== undefined && (o.alias || o.near !== undefined)) {\n throw new PathError(`\"${o.path}\" orders by a column's own text only when it is a column.`, o.path, o.path, 0);\n }\n if (o.alias) {\n /* Refused by name rather than left to the engine: SQLite quietly\n treats an unknown ORDER BY identifier as a new error anyway, but\n \"no such column: invoicez\" would not say the fix is the alias two\n words back. */\n if (!aliasNames.has(o.path)) {\n throw new PathError(`\"${o.path}\" is not an aggregate alias on this view.`, o.path, o.path, 0);\n }\n return `${style.quote(o.path)} ${o.direction === 'desc' ? 'DESC' : 'ASC'}`;\n }\n if (o.near !== undefined) {\n /* Closest first, as a literal rather than a parameter: the ORDER BY is\n written before the WHERE's parameters are numbered, and a finite,\n safe number carries nothing but itself. Parenthesized, so a negative\n value never meets the minus as \"--\", a comment in SQL. */\n if (typeof o.near !== 'number' || !Number.isFinite(o.near) || Math.abs(o.near) > Number.MAX_SAFE_INTEGER) {\n throw new PathError(`\"near\" on \"${o.path}\" must be a finite number.`, o.path, o.path, 0);\n }\n return `ABS(${qualify(o.path)} - (${String(o.near)})) ${o.direction === 'desc' ? 'DESC' : 'ASC'}`;\n }\n /* A vocabulary sorts as declared (schema.ts declaredOrder); an order by\n bytes asked for the bytes, for a cursor that compares them. */\n if (o.collation === undefined)\n return { expression: qualify(o.path), direction: o.direction, declared: declaredOrder(resolve(o.path).column) };\n return `${collate(qualify(o.path), o.collation, style)} ${o.direction === 'desc' ? 'DESC' : 'ASC'}`;\n }\n\n const limit = clampLimit(view.limit);\n const skip = clampOffset(offset);\n const joins = plan.clauses();\n const prefix = relations.finish();\n /* The other side of a column comparison is a path like any other: resolved\n against this catalog, so a reader's pruned graph refuses one it may not\n read exactly as it refuses the left. */\n const where = buildWhere(conditions, style, prefix.params.length + 1, qualify, qualify, membership, exists, false, typeOf);\n\n /* Placeholder numbering continues across the whole statement, which is the\n one thing Postgres needs and SQLite does not — and the exact place a join\n builder gets it wrong, because the joins sit between the SELECT list and\n the WHERE clause and are easy to count as if they carried parameters. */\n const sorted = orderClauseSql(order, style, prefix.params.length + where.params.length + 1);\n const n = prefix.params.length + where.params.length + sorted.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.replace(/^ WHERE /, ' WHERE '));\n if (sorted.text) lines.push(sorted.text);\n lines.push(` LIMIT ${style.placeholder(n + 1)} OFFSET ${style.placeholder(n + 2)}`);\n\n return {\n text: prefix.text + lines.join('\\n'),\n params: [...prefix.params, ...where.params, ...sorted.params, limit, skip],\n columns,\n joins: plan.joins,\n limit,\n };\n}\n\n/* ---------- the query language, over paths ----------\n\n The composer's filter box takes the same language as the bar at the top,\n because two languages for one idea is one too many. It is parsed by the\n same parser, too: `parseQuery` already handles and/or, parentheses, quoting,\n relative dates and per-column value coercion, and a second implementation\n of those would be a second set of bugs.\n\n The trick that makes it work is a *synthetic* table. `parseQuery` resolves\n every field against a table's column list, so the base table is handed to it\n with one extra column per path mentioned in the text, each carrying the\n declared type of the column the path actually ends on. Coercion, date\n handling and \"did you mean\" then work over paths for free.\n\n Paths are resolved on demand rather than enumerated: `work_order.parent_id`\n points back at `work_order`, so the set of paths four hops deep is tens of\n thousands of entries and building it per keystroke would be absurd. */\n\n/* One segment of a path: a column name, or — for a composite key — the\n columns of it joined with `+`, exactly as `hopName` writes them.\n\n `+` has to be admitted here as well as in `parseQuery`'s FIELD, or the two\n disagree about where a token begins: the scanner reads only `line_no.sku`\n out of `invoice_id+line_no.sku`, builds the synthetic column for a path that\n does not exist, and a composite hop becomes something you can show but never\n filter. It is legal only *between* identifier characters, never leading —\n a leading `+` would let a signed number start a path. */\nconst SEGMENT = String.raw`[A-Za-z_$][\\w$]*(?:\\+[A-Za-z_$][\\w$]*)*`;\n\n/* A dotted token. `@` and `+` are excluded from the lookbehind on purpose:\n without `@`, `email = ops@harbour.example` reads its own value as a path and\n reports a confident error about a table that was never mentioned; without\n `+`, the tail of a composite hop matches on its own and reports the same\n kind of confident error about half a key. */\nconst DOTTED_TOKEN = new RegExp(String.raw`(?<![\\w.$@+])(${SEGMENT}(?:\\.${SEGMENT})+)`, 'g');\n\n/** What makes a token a *field* rather than a value: an operator after it. */\nconst OPERATOR_AFTER = /^\\s*(?:!=|<=|>=|=|<|>|\\bcontains\\b|\\bstartswith\\b|\\bendswith\\b|\\blike\\b|\\bnot\\s+in\\b|\\bin\\b|\\bis\\b)/i;\n\n/** …or a word before it that can only be followed by a field. */\nconst FIELD_BEFORE = /(?:^|[\\s(,])(?:and|or|where|filter|show|select|sort|by)\\s*$|(?:^|,)\\s*$/i;\n\nexport interface ParsedView {\n filter: ViewFilter;\n columns: string[];\n orderBy: ViewOrder[];\n limit?: number;\n errors: QueryError[];\n}\n\n/**\n * Parse the composer's filter/clause text against a base table.\n *\n * Returns errors rather than throwing, because this runs on every keystroke\n * and a half-typed path is the normal state of the input, not an exception.\n */\nexport function parseViewQuery(\n schema: DatabaseSchema,\n baseId: string,\n text: string,\n now?: Date,\n options: {\n aggregateAliases?: string[];\n /** Who `me` is for this request; null, nobody signed in; absent, `me` is refused (query.ts). */\n me?: QueryPerson | null;\n /** Admit a column on a condition's right-hand side (the default); an API's standing query does not. */\n columnValues?: boolean;\n } = {},\n): ParsedView {\n const empty: ParsedView = { filter: { groups: [] }, columns: [], orderBy: [], errors: [] };\n const base = findTable(schema, baseId);\n if (!base) {\n return { ...empty, errors: [{ message: `No table called \"${baseId}\" in this database.` }] };\n }\n const written = String(text ?? '').trim();\n if (!written) return empty;\n\n const errors: QueryError[] = [];\n /* A segment's name is written out first (query.ts expandSegments), so the paths, rollups and computed\n columns its conditions name are found below as if they had been typed; a position is said in the text\n as written. */\n let body = written;\n let said = (at: number) => at;\n if (base.segments) {\n const { head } = splitClauseTail(written);\n const start = head ? written.indexOf(head) : -1;\n if (start >= 0) {\n const expanded = expandSegments(head, base.segments);\n if (expanded.error) return { ...empty, errors: [{ message: expanded.error.message, at: start + (expanded.error.at ?? 0) }] };\n if (expanded.text !== head) {\n body = written.slice(0, start) + expanded.text + written.slice(start + head.length);\n const end = start + expanded.text.length;\n said = (at) => (at < start ? at : at < end ? start + expanded.at(at - start) : at - (expanded.text.length - head.length));\n }\n }\n }\n const extra: ColumnMeta[] = [];\n /* Each `any`/`no` condition is read on its own, against the child, and stands in the text the parser reads as a\n synthetic column asked `is (not) empty`; it becomes the condition it is when the parse comes back. */\n const anys = new Map<string, ViewCondition>();\n if (/\\b(?:any|no)\\s/i.test(body)) {\n const read = anyRowsOf(schema, base, body, now, options, anys, extra);\n if (read.errors.length)\n return { ...empty, errors: read.errors.map((error) => ({ ...error, at: error.at === undefined ? undefined : said(error.at) })) };\n if (read.text !== body) {\n const outer = said;\n body = read.text;\n said = (at) => outer(read.at(at));\n }\n }\n const added = new Set<string>();\n /* Paths already reported as broken. The synthetic table cannot contain\n them, so the parser will reach them too and say \"not a column on invoice\"\n — true, useless, and directly under a message that named the real\n problem. Two errors for one mistake reads as two mistakes. */\n const failed = new Set<string>();\n\n const scanned = maskQuoted(body);\n for (const match of scanned.matchAll(DOTTED_TOKEN)) {\n const path = match[1];\n const start = match.index ?? 0;\n /* Whether this token is being used as a field decides whether a failure\n to resolve is worth reporting. In value position — `external_ref =\n a.b` — a dotted word that is not a path is simply a value, and an error\n about it would be noise on a query that is perfectly correct. */\n /* …or a path on the right of a comparison whose first hop is a column\n of the base: `on_hand < product_id.reorder_level` compares with it,\n so a hop that goes nowhere is a mistake to name, not text. */\n const isField =\n OPERATOR_AFTER.test(scanned.slice(start + path.length)) ||\n FIELD_BEFORE.test(scanned.slice(0, start)) ||\n (/(?:!=|<=|>=|=|<|>)\\s*$/.test(scanned.slice(0, start)) && base.columns.some((c) => c.name === path.split('.')[0].split('+')[0]));\n if (added.has(path)) continue;\n added.add(path);\n const { resolved, error } = tryResolvePath(schema, baseId, path);\n if (error) {\n /* Located at the token so the composer can underline the right word.\n The offset is into the filter text, and the caller shifts it. */\n if (isField) {\n errors.push({ message: error.message, at: said(start) });\n failed.add(path);\n }\n continue;\n }\n if (!resolved) continue;\n /* The synthetic column keeps the *declared* type of the real column, so\n `signed_up = last 30 days` through a path behaves exactly as it does\n on the base table. Nullability is widened because the join can miss. */\n extra.push({\n name: resolved.path,\n type: resolved.column.type,\n nullable: true,\n primaryKey: false,\n references: resolved.column.references,\n });\n }\n\n /* The base's own record label (`show $label`, `$label contains 12`) is a\n column of the synthetic table where the catalog serves a template. */\n if (base.labelTemplate && /(?<![\\w.$])\\$label(?![\\w$])/.test(scanned)) {\n extra.push(labelColumnMeta());\n }\n\n /* The base's rollups (rollups.ts) are columns of the synthetic table where\n the text names one, typed as the catalog serves them, so `sort\n line_total desc` and `open_deals > 0` parse and coerce like any number.\n A rollup through a reference is a dotted path, found above. */\n for (const [name, rollup] of Object.entries(base.rollups ?? {})) {\n if (added.has(name) || base.columns.some((c) => c.name === name)) continue;\n if (new RegExp(`(?<![\\\\w.$])${name}(?![\\\\w$])`).test(scanned)) extra.push(rollupColumnMeta(name, rollup));\n }\n /* And its computed columns (computed.ts), typed as they come out: `is_overdue = true`, `sort days_open desc`. */\n for (const [name, computed] of Object.entries(base.computed ?? {})) {\n if (added.has(name) || base.columns.some((c) => c.name === name)) continue;\n if (new RegExp(`(?<![\\\\w.$])${name}(?![\\\\w$])`).test(scanned)) extra.push(computedColumnMeta(name, computed));\n }\n /* And its tags (tagLists.ts), a column only `has` may ask: `tags has bug`, `show name, tags`. */\n if (\n base.tags &&\n !added.has(TAGS_COLUMN) &&\n !base.columns.some((c) => c.name === TAGS_COLUMN) &&\n new RegExp(`(?<![\\\\w.$])${TAGS_COLUMN}(?![\\\\w$])`).test(scanned)\n )\n extra.push(tagsColumnMeta());\n\n /* Aggregate aliases join the synthetic table so `sort invoices desc`\n parses — and only sort. A WHERE runs before the SELECT list exists, so\n filtering by an alias is refused below with the reason, not with\n \"not a column\", which is true and useless. */\n const aliases = new Set(options.aggregateAliases ?? []);\n for (const alias of aliases) {\n if (!added.has(alias)) extra.push({ name: alias, type: 'NUMERIC', nullable: true, primaryKey: false });\n }\n\n const synthetic: DatabaseSchema = {\n ...schema,\n tables: schema.tables.map((t) => (t.id === base.id ? { ...t, columns: [...t.columns, ...extra] } : t)),\n };\n\n /* The parser wants `<table> <clauses>`, which is also what the composer's\n hint text shows, so the base name is prepended rather than the grammar\n being special-cased. */\n const offset = base.id.length + 1;\n const parsed = parseQuery(`${base.id} ${body}`, {\n schema: synthetic,\n now,\n columnValues: options.columnValues !== false,\n ...(options.me !== undefined ? { me: options.me } : {}),\n });\n for (const err of parsed.errors) {\n if ([...failed].some((path) => err.message.includes(`\"${path}\"`))) continue;\n errors.push({ message: err.message, at: err.at === undefined ? undefined : said(Math.max(0, err.at - offset)) });\n }\n\n const query = parsed.query;\n if (!query) return { ...empty, errors };\n\n for (const group of query.filter.groups) {\n for (const c of group) {\n if (aliases.has(c.column)) {\n errors.push({\n message: `\"${c.column}\" is an aggregate result, and a filter runs before it exists. ` + 'Filter on the child table instead.',\n });\n }\n }\n }\n for (const column of query.columns ?? []) {\n if (aliases.has(column)) {\n errors.push({ message: `\"${column}\" is already shown — the aggregate adds its own column.` });\n }\n }\n\n return {\n filter: {\n groups: query.filter.groups.map((group) =>\n group.map(\n (c): ViewCondition =>\n anys.get(c.column) ??\n (c.other === undefined ? { path: c.column, op: c.op, value: c.value } : { path: c.column, op: c.op, other: c.other }),\n ),\n ),\n },\n columns: query.columns,\n orderBy: query.orderBy.map((o) =>\n aliases.has(o.column) ? { path: o.column, direction: o.direction, alias: true } : { path: o.column, direction: o.direction },\n ),\n limit: query.limit,\n errors,\n };\n}\n\n/* The name an `any` condition stands under while the parser reads the rest: no column can be called it. */\nconst ANY_STANDIN = '$any';\n\n/**\n * The `any`/`no` conditions of a view's text, each read against its child\n * table: the text with each written as a stand-in column asked `is (not)\n * empty`, the stand-ins (in `extra`) and the conditions they become (in\n * `anys`), and a position in the new text said in the old.\n *\n * The child is the table whose one reference points at the base, or the one\n * `via` names; its conditions are its own columns and paths up from it, and\n * its segments — what a row scope's narrowing reads (rowScope.ts `filtered`),\n * which is how the EXISTS is written. A second `any` inside, a rollup, a\n * computed column, tags or a record label are refused by name rather than\n * left to fail as SQL.\n */\nfunction anyRowsOf(\n schema: DatabaseSchema,\n base: TableMeta,\n body: string,\n now: Date | undefined,\n options: { me?: QueryPerson | null; columnValues?: boolean },\n anys: Map<string, ViewCondition>,\n extra: ColumnMeta[],\n): { text: string; at: (position: number) => number; errors: QueryError[] } {\n const same = { text: body, at: (position: number) => position, errors: [] as QueryError[] };\n const { head } = splitClauseTail(body);\n const start = head ? body.indexOf(head) : -1;\n if (start < 0) return same;\n const found = anyConditions(head);\n if (!found.length) return same;\n const errors: QueryError[] = [];\n const splices: Array<{ at: number; was: number; from: number; length: number }> = [];\n let text = '';\n let last = 0;\n for (const condition of found) {\n const at = start + condition.at;\n const [, word, childName, via, rest] = ANY_CONDITION.exec(condition.text)!;\n const fail = (message: string, offset = 0) => errors.push({ message, at: at + offset });\n const named = backrefsTo(schema, base.id).filter((b) => {\n const table = findTable(schema, b.child);\n return table && (table.id === childName || table.name === childName);\n });\n const through = via === undefined ? named : named.filter((b) => b.fk.from.columns.join('+') === via);\n if (!named.length) {\n fail(\n findTable(schema, childName) || schema.tables.some((t) => t.name === childName)\n ? `${childName} has no reference to ${base.name}, so \"${word} ${childName}\" has no rows to ask about.`\n : `No table called \"${childName}\" here.`,\n word.length + 1,\n );\n continue;\n }\n if (!through.length) {\n fail(\n `${childName} points at ${base.name} through ${named.map((b) => b.fk.from.columns.join('+')).join(', ')}, not \"${via}\".`,\n condition.text.indexOf(via!, word.length + childName.length + 1),\n );\n continue;\n }\n if (through.length > 1) {\n const columns = through.map((b) => b.fk.from.columns.join('+'));\n fail(\n `${childName} points at ${base.name} through ${columns.join(' and ')}: say which — ${word} ${childName} via ${columns[0]}.`,\n word.length + 1,\n );\n continue;\n }\n const { fk } = through[0];\n if (fk.stitched) {\n fail(\n `${childName} is in another source, on another connection, so a list of ${base.name} cannot ask about its rows: ` +\n 'each database filters what it holds.',\n word.length + 1,\n );\n continue;\n }\n const child = findTable(schema, fk.from.table)!;\n let filter: Filter | undefined;\n if (rest !== undefined) {\n const restAt = condition.text.length - rest.length;\n const parsed = parseViewQuery(schema, child.id, rest, now, { me: options.me, columnValues: options.columnValues });\n for (const error of parsed.errors) fail(error.message, restAt + (error.at ?? 0));\n if (parsed.errors.length) continue;\n if (parsed.columns.length || parsed.orderBy.length || parsed.limit !== undefined) {\n fail(`\"${word} ${childName}\" takes conditions only: a show, sort or limit there has no rows to shape.`, restAt);\n continue;\n }\n const unread = parsed.filter.groups\n .flat()\n .flatMap((c) => (c.any ? [c.path] : [c.path, ...(c.other === undefined ? [] : [c.other])]))\n .find((path) => {\n if (path.startsWith('any ') || path.startsWith('no ')) return true;\n const { resolved } = tryResolvePath(schema, child.id, path);\n return !resolved || Boolean(resolved.rollup || resolved.computed || resolved.tags || resolved.jsonKey || path.includes('$'));\n });\n if (unread !== undefined) {\n fail(\n /^(?:any|no)\\s/i.test(unread)\n ? `\"${word} ${childName}\" cannot ask about rows of its own rows: say that as its own condition.`\n : `\"${unread}\" is not a stored column of ${child.name}, and \"${word} ${childName}\" reads only those.`,\n restAt + Math.max(0, rest.indexOf(unread)),\n );\n continue;\n }\n const stitched = stitchedListProblem(schema, child.id, { filter: parsed.filter });\n if (stitched) {\n fail(stitched, restAt);\n continue;\n }\n filter = {\n groups: parsed.filter.groups.map((group) =>\n group.map((c): Condition =>\n c.other === undefined ? { column: c.path, op: c.op, value: c.value } : { column: c.path, op: c.op, other: c.other },\n ),\n ),\n };\n }\n const standin = `${ANY_STANDIN}${anys.size}`;\n const said = condition.text.replace(/\\s+/g, ' ');\n anys.set(standin, {\n path: said,\n op: word.toLowerCase() === 'no' ? 'isNull' : 'isNotNull',\n any: { via: `${child.id}.${fk.from.columns.join('+')}`, ...(filter ? { filter } : {}) },\n });\n extra.push({ name: standin, type: 'INTEGER', nullable: true, primaryKey: false });\n const written = `${standin} ${word.toLowerCase() === 'no' ? 'is empty' : 'is not empty'}`;\n text += body.slice(last, at);\n splices.push({ at, was: condition.text.length, from: text.length, length: written.length });\n text += written;\n last = at + condition.text.length;\n }\n if (errors.length) return { ...same, errors };\n text += body.slice(last);\n return {\n text,\n errors,\n at: (position) => {\n let shift = 0;\n for (const splice of splices) {\n if (position < splice.from) break;\n if (position < splice.from + splice.length) return splice.at;\n shift = splice.from + splice.length - (splice.at + splice.was);\n }\n return position - shift;\n },\n };\n}\n\n/**\n * How many rows the view matches, ignoring the page.\n *\n * A separate compilation rather than a wrapper around the other one, because\n * the two differ in more than a keyword: the SELECT list, the ORDER BY and the\n * page are all irrelevant here, and every aggregate in the view would be a\n * correlated subquery evaluated per row for a number nobody reads.\n *\n * The count is exact rather than estimated, and it is *base rows* — which is\n * only true because every join is to-one. That is the same property the whole\n * design rests on: a view never multiplies, so counting it is counting the\n * table it started from. With `limit`, it stops after that many.\n */\nexport function compileViewCount(schema: DatabaseSchema, view: ViewDef, style: SqlStyle, scope?: RowScope, limit?: number): SqlFragment {\n schema = withViewReferences(schema, view.references);\n const base = findTable(schema, view.base);\n if (!base) {\n throw new PathError(`No table called \"${view.base}\" in this database, so the view has nothing to start from.`, view.base, view.base, 0);\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 const { qualify, membership, exists, typeOf } = viewPaths(schema, view, plan, style, relations);\n\n const conditions = viewConditions(view);\n for (const group of conditions?.groups ?? []) {\n for (const condition of group) {\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 const joins = plan.clauses();\n const prefix = relations.finish();\n const where = buildWhere(conditions, style, prefix.params.length + 1, qualify, qualify, membership, exists, false, typeOf);\n\n /* Only filter joins affect row count. Projection-only joins and scalar\n aggregates deliberately do not register relations in this statement. */\n const relation = `${baseRelation} ${style.quote(plan.baseAlias)}` + (joins.length ? `\\n${joins.join('\\n')}` : '') + where.text;\n /* A bound stops the count the way every bounded count stops\n (countStatement): at most `limit` base rows are read. */\n const text =\n limit === undefined ? prefix.text + `SELECT COUNT(*) AS n\\n FROM ${relation}` : prefix.text + countStatement(relation, limit);\n return { text, params: [...prefix.params, ...where.params] };\n}\n\n/**\n * Render a view as the SQL it will become, values inline.\n *\n * The mirror of `explain` in query.ts and used the same way: this string never\n * reaches a database, and showing `\"customer_1\".\"name\" LIKE '%Harbour%'` next\n * to the path that produced it is how someone learns what the composer is\n * doing on their behalf.\n */\nexport function explainView(schema: DatabaseSchema, view: ViewDef, style: SqlStyle): string {\n const compiled = compileView(schema, withViewComputed(schema, withViewLabels(schema, view)), style);\n let i = 0;\n return compiled.text.replace(/\\$\\d+|\\?/g, () => sqlLiteral(compiled.params[i++]));\n}\n\n/**\n * One parameter, written the way SQL would have to spell it.\n *\n * Exported because a breakdown explains itself the same way a view does, and\n * two spellings of `NULL` — or two different ideas about escaping a quote —\n * would show up as a difference between two panes that claim to be the same\n * statement.\n */\nexport function sqlLiteral(value: unknown): string {\n if (value === null || value === undefined) return 'NULL';\n if (typeof value === 'number' || typeof value === 'boolean') return String(value);\n return `'${String(value).replace(/'/g, \"''\")}'`;\n}\n\n/* ---------- config file support ----------\n\n Views can be predefined in `tablewalk.json` beside `connections`, and are\n validated the same strict way: a malformed entry is an error naming the\n entry, never a silent skip. Quietly dropping a view someone wrote is the\n worst outcome — they see a shorter list and no reason for it. */\n\nexport function parseViewsConfig(raw: unknown, where: string): ViewDef[] {\n const views = (raw as { views?: unknown })?.views;\n if (views === undefined) return [];\n if (!Array.isArray(views)) throw new Error(`${where}: \"views\" must be an array.`);\n\n const seen = new Set<string>();\n return views.map((entry, i) => {\n const at = `${where}: view ${i + 1}`;\n if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {\n throw new Error(`${at} is not an object.`);\n }\n const v = entry as Record<string, unknown>;\n if (typeof v.name !== 'string' || !v.name.trim()) throw new Error(`${at} has no \"name\".`);\n\n /* A view may be written down instead of built out of objects. The text\n form needs a schema to resolve, which a config file does not have, so\n it is carried through and expanded when a connection is open — and any\n error it holds is reported against that view rather than swallowed. */\n const text = v.text === undefined ? undefined : String(v.text).trim();\n if (v.text !== undefined && (typeof v.text !== 'string' || !text)) {\n throw new Error(`${at} (\"${v.name}\") has a \"text\" that is not a non-empty string.`);\n }\n if (text && v.base !== undefined) {\n throw new Error(`${at} (\"${v.name}\") has both \"text\" and \"base\". The text form already names its table.`);\n }\n if (!text && (typeof v.base !== 'string' || !v.base.trim())) {\n throw new Error(`${at} (\"${v.name}\") has no \"base\" table.`);\n }\n const id = v.id === undefined ? `config-${slug(v.name, 'view')}` : String(v.id);\n if (v.id !== undefined && (typeof v.id !== 'string' || !v.id.trim())) {\n throw new Error(`${at} (\"${v.name}\") has an \"id\" that is not a non-empty string.`);\n }\n if (seen.has(id)) throw new Error(`${at} (\"${v.name}\") repeats the id \"${id}\".`);\n seen.add(id);\n\n const columns = parseColumns(v.columns, at, String(v.name));\n const aggregates = parseAggregates(v.aggregates, at, String(v.name));\n const filter = parseFilter(v.filter, at, String(v.name));\n const orderBy = parseOrder(v.orderBy, at, String(v.name));\n\n if (v.limit !== undefined && (typeof v.limit !== 'number' || !Number.isFinite(v.limit))) {\n throw new Error(`${at} (\"${v.name}\") has a \"limit\" that is not a number.`);\n }\n if (v.connection !== undefined && typeof v.connection !== 'string') {\n throw new Error(`${at} (\"${v.name}\") has a \"connection\" that is not a string.`);\n }\n\n return {\n id,\n name: v.name.trim(),\n base: text ? text.trim().split(/\\s+/)[0] : (v.base as string).trim(),\n text,\n columns,\n aggregates,\n filter,\n orderBy,\n limit: v.limit as number | undefined,\n connection: v.connection as string | undefined,\n source: 'config' as const,\n };\n });\n}\n\nfunction parseColumns(raw: unknown, at: string, name: string): ViewColumn[] {\n if (raw === undefined) return [];\n if (!Array.isArray(raw)) throw new Error(`${at} (\"${name}\") has \"columns\" that is not an array.`);\n return raw.map((col, j) => {\n const where = `${at} (\"${name}\"), column ${j + 1}`;\n if (typeof col === 'string') {\n if (!col.trim()) throw new Error(`${where} is empty.`);\n return { path: col.trim() };\n }\n if (!col || typeof col !== 'object') throw new Error(`${where} is not a string or an object.`);\n const c = col as Record<string, unknown>;\n if (typeof c.path !== 'string' || !c.path.trim()) throw new Error(`${where} has no \"path\".`);\n if (c.alias !== undefined && (typeof c.alias !== 'string' || !c.alias.trim())) {\n throw new Error(`${where} has an \"alias\" that is not a non-empty string.`);\n }\n return { path: c.path.trim(), alias: c.alias as string | undefined };\n });\n}\n\nfunction parseAggregates(raw: unknown, at: string, name: string): ViewAggregate[] | undefined {\n if (raw === undefined) return undefined;\n if (!Array.isArray(raw)) throw new Error(`${at} (\"${name}\") has \"aggregates\" that is not an array.`);\n return raw.map((entry, j) => {\n const where = `${at} (\"${name}\"), aggregate ${j + 1}`;\n if (!entry || typeof entry !== 'object') throw new Error(`${where} is not an object.`);\n const a = entry as Record<string, unknown>;\n if (typeof a.fn !== 'string' || !AGGREGATE_FNS.includes(a.fn as AggregateFn)) {\n throw new Error(`${where} has an unknown \"fn\". Use one of: ${AGGREGATE_FNS.join(', ')}.`);\n }\n if (typeof a.via !== 'string' || !a.via.trim()) throw new Error(`${where} has no \"via\".`);\n if (a.fn !== 'count' && (typeof a.column !== 'string' || !a.column.trim())) {\n throw new Error(`${where} is a ${a.fn} and needs a \"column\" to summarise.`);\n }\n if (a.on !== undefined && typeof a.on !== 'string') {\n throw new Error(`${where} has an \"on\" that is not a string.`);\n }\n if (a.alias !== undefined && (typeof a.alias !== 'string' || !a.alias.trim())) {\n throw new Error(`${where} has an \"alias\" that is not a non-empty string.`);\n }\n return {\n fn: a.fn as AggregateFn,\n via: a.via.trim(),\n on: a.on as string | undefined,\n column: typeof a.column === 'string' ? a.column.trim() : undefined,\n alias: a.alias as string | undefined,\n };\n });\n}\n\nconst OPS: Op[] = ['=', '!=', '>', '>=', '<', '<=', 'contains', 'startsWith', 'endsWith', 'like', 'in', 'isNull', 'isNotNull'];\n\nfunction parseFilter(raw: unknown, at: string, name: string): ViewFilter | undefined {\n if (raw === undefined) return undefined;\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {\n throw new Error(`${at} (\"${name}\") has a \"filter\" that is not an object.`);\n }\n const groups = (raw as { groups?: unknown }).groups;\n if (!Array.isArray(groups)) throw new Error(`${at} (\"${name}\") has a \"filter\" with no \"groups\" array.`);\n return {\n groups: groups.map((group, g) => {\n if (!Array.isArray(group)) throw new Error(`${at} (\"${name}\"), filter group ${g + 1} is not an array.`);\n return group.map((cond, c) => {\n const where = `${at} (\"${name}\"), filter group ${g + 1} condition ${c + 1}`;\n if (!cond || typeof cond !== 'object') throw new Error(`${where} is not an object.`);\n const x = cond as Record<string, unknown>;\n if (typeof x.path !== 'string' || !x.path.trim()) throw new Error(`${where} has no \"path\".`);\n if (typeof x.op !== 'string' || !OPS.includes(x.op as Op)) {\n throw new Error(`${where} has an unknown operator \"${String(x.op)}\". Use one of: ${OPS.join(', ')}.`);\n }\n if (x.other !== undefined && (typeof x.other !== 'string' || !x.other.trim())) {\n throw new Error(`${where} has an \"other\" that is not a path.`);\n }\n return typeof x.other === 'string'\n ? { path: x.path.trim(), op: x.op as Op, other: x.other.trim() }\n : { path: x.path.trim(), op: x.op as Op, value: x.value as ViewCondition['value'] };\n });\n }),\n };\n}\n\nfunction parseOrder(raw: unknown, at: string, name: string): ViewOrder[] | undefined {\n if (raw === undefined) return undefined;\n if (!Array.isArray(raw)) throw new Error(`${at} (\"${name}\") has an \"orderBy\" that is not an array.`);\n return raw.map((entry, j) => {\n const where = `${at} (\"${name}\"), orderBy ${j + 1}`;\n if (typeof entry === 'string') return { path: entry, direction: 'asc' as const };\n if (!entry || typeof entry !== 'object') throw new Error(`${where} is not a string or an object.`);\n const o = entry as Record<string, unknown>;\n if (typeof o.path !== 'string' || !o.path.trim()) throw new Error(`${where} has no \"path\".`);\n const direction = o.direction === undefined ? 'asc' : o.direction;\n if (direction !== 'asc' && direction !== 'desc') {\n throw new Error(`${where} has a \"direction\" that is not \"asc\" or \"desc\".`);\n }\n return { path: o.path.trim(), direction };\n });\n}\n\n/**\n * Check a view against a schema without running it.\n *\n * Used by `GET /api/views` so a config view naming a table that does not exist\n * on this connection is listed *with its reason*, rather than either vanishing\n * or blowing up the endpoint for every other view beside it.\n */\nexport function validateView(schema: DatabaseSchema, view: ViewDef): string | undefined {\n try {\n /* Any style will do: this compiles to throw away the text and keep the\n error, and no dialect refuses a view another accepts. */\n compileView(schema, view, ANSI_STYLE);\n return undefined;\n } catch (err) {\n return (err as Error).message;\n }\n}\n","/** What core says, in the shell room (packages/kit/src/client/messages.ts says how the catalog is split). */\nimport { defineMessages } from '../../messages.js';\n\nexport const say = defineMessages({\n searchIsNotAnswering: 'Search is not answering right now, so this searched the database instead.',\n isNotAnswering: '{app} is not answering. The server it was started with may have stopped — restart it and reload this page.',\n theServerIsNotAnswering: 'The server is not answering right now. Check your connection, then try again.',\n useACanonicalApi: 'Use a canonical API path without a fragment.',\n invalidOrdinaryUndoIdentity: 'Invalid ordinary Undo identity.',\n yourSessionExpiredReload: 'Your session expired. Reload to continue.',\n theSignedInAccount: 'The signed-in account changed.',\n theServerAnsweredWithSomething: 'The server answered {status} with something that is not JSON. ',\n somethingBetweenThisPage: 'Something between this page and {app} may be intercepting the request.',\n waitForThePending: 'Wait for the pending record update before leaving this view.',\n theTenantContextIsUnavailable: 'The tenant context is unavailable. Verify your session and choose a tenant again.',\n aChangeIsPending: 'A change is pending or unconfirmed. Resolve it before leaving this view.',\n fixTheQueryBefore: 'Fix the query before saving it as a filter.',\n forTheAuthor: '{message} Why: {cause}',\n});\n","/**\n * The parts every other client module needs: the API, the navigation stack,\n * the schema mirrors, and DOM building (dom.js, re-exported here).\n *\n * Apart from independent connection-transition, notification and DOM helpers, this\n * file depends on no other client modules. Everything else depends on it,\n * which is what keeps the module\n * graph a fan-out rather than a web — and lets a feature live in its own file\n * without reaching into the renderer.\n */\n\nimport { flushSync } from 'react-dom';\nimport { DEFAULT_LIMIT } from '@mistriwork/engine/shared/reads.js';\nimport { PAGE_HEADER } from '@mistriwork/engine/shared/pageRoutes.js';\nimport { quoteValue } from '@mistriwork/engine/shared/query.js';\nimport { connectionGeneration } from './connectionTransition.js';\nimport { showToast } from './toast.js';\nimport { scopeApiRequest } from './requestIdentity.js';\nimport { assertSessionCurrent, captureScopedStorage, captureSessionIdentity, sessionIdentity } from './sessionIdentity.js';\nimport { invalidateSession } from './sessionLifecycle.js';\nimport { shell } from './shellMode.js';\nimport { historyToken } from './historyToken.js';\nimport { beginWork } from './pendingWork.js';\nimport { composedReads, setComposedConnection } from './composedHook.js';\nimport { isWritePath } from './writePaths.js';\nimport { firstPaintRead, forgetFirstPaint } from './firstPaint.js';\nimport { ownWriteSent } from './ownWrites.js';\nimport { trailFor } from './recordTrail.js';\nimport { disclosure, el, svgEl, trapFocus } from './dom.js';\nimport {\n columnKind,\n findTable as sharedFindTable,\n labelColumn,\n labelPath as sharedLabelPath,\n looksLikeDate,\n primaryKey,\n referencesFrom,\n referencesTo,\n} from '@mistriwork/engine/shared/schema.js';\nimport { hopName, hopsFrom as sharedHopsFrom, tableAtPrefix as sharedTableAtPrefix } from '@mistriwork/engine/shared/view.js';\nimport { say } from './copy/shell/core.js';\nimport { shellById, shellQuery, shellQueryAll, shellRoot } from './shellHost.js';\nimport { fetchServer } from './transport.js';\nimport { fileReads } from './liveReads.js';\nexport { andQuery, splitClauseTail } from '@mistriwork/engine/shared/query.js';\nexport { humanName } from '@mistriwork/engine/shared/model.js';\n/* Building DOM and the popups' keyboard manners live in dom.js; re-exported\n so every caller keeps its import. */\nexport { disclosure, el, svgEl, trapFocus };\n\n/** @overload @param {'query' | 'table-filter'} id @returns {HTMLInputElement} */\n/** @overload @param {'querybar'} id @returns {HTMLFormElement} */\n/** @overload @param {string} id @returns {HTMLElement} */\n/** @param {string} id */\nexport function $(id) {\n return shellById(id);\n}\n\n/**\n * @type {{schema: any, stack: any[], page: number, canWrite: boolean, activeConnection?: string | null, navMode: string,\n * connections?: (import('@mistriwork/engine/shared/connection.js').AppSource & Partial<import('@mistriwork/engine/shared/connection.js').PublicConnection>)[],\n * onSwitched?: (isCurrent?: () => boolean) => void | Promise<void>}}\n *\n * `stack` is a navigation stack, and each entry is somewhere you have been —\n * a table with a filter, or one row. The breadcrumb is that stack rendered,\n * so walking back restores the filter you walked away from.\n */\nexport const state = { schema: null, stack: [], page: 0, canWrite: false, navMode: 'replace' };\n/* A record page's composed answers (composedHook.ts) go to this connection when a request names none. */\nsetComposedConnection(() => state.activeConnection ?? null);\n\n/* The server's DEFAULT_LIMIT. Small on purpose: the first page of a\n browse should be instant, and paging is one click. */\nconst PAGE_SIZE = DEFAULT_LIMIT;\n\n/**\n * How many rows a page actually holds, which the reader chooses.\n *\n * `PAGE_SIZE` stays the default and stays equal to the server's\n * `DEFAULT_LIMIT` — twenty is right for a first look, and the first look is\n * what most browsing is. It was also the *only* size, which turned a\n * sixty-thousand-row table into three thousand clicks: a table you could read\n * the front of and nothing else.\n *\n * Capped at 250 rather than offering \"all\". The grid renders every row it is\n * given, so the honest ceiling is the one where the DOM is still the reader's\n * friend; past that the answer is a filter, a sort, or Export.\n */\nexport const PAGE_SIZES = [10, 15, 20, 50, 100, 250];\nconst PAGE_SIZE_KEY = 'mistri.page-size';\nlet chosenPageSize = null;\n\nexport function pageSize() {\n if (chosenPageSize === null) {\n const stored = Number(loadJson(PAGE_SIZE_KEY, PAGE_SIZE));\n /* Validated rather than trusted: the value comes from localStorage, where\n a stale build or a hand edit can leave anything at all, and a page size\n of `null` asks the server for `LIMIT NaN`. */\n chosenPageSize = PAGE_SIZES.includes(stored) ? stored : PAGE_SIZE;\n }\n return chosenPageSize;\n}\n\nexport function setPageSize(size) {\n chosenPageSize = PAGE_SIZES.includes(Number(size)) ? Number(size) : PAGE_SIZE;\n saveJson(PAGE_SIZE_KEY, chosenPageSize);\n /* A page *number* means nothing once the size changes — page 40 of twenty\n is not page 40 of a hundred — so the only honest place to land is the\n front. */\n state.page = 0;\n}\n\n/* ---------- API ---------- */\n\n/* An App's search engine did not answer, so a text filter ran on the\n database (the server's `x-mistri-search: database`). Said once per page\n load: the results are still right, only found the slower way. */\nlet searchFallbackSaid = false;\n/** @param {Response} res */\nfunction noteSearchFallback(res) {\n if (searchFallbackSaid || res.headers?.get('x-mistri-search') !== 'database') return;\n searchFallbackSaid = true;\n toast(say('searchIsNotAnswering'));\n}\n\n/**\n * What a reader is told when the server does not answer at all. Offline\n * comes first, because it is the one cause the reader can see and fix; a\n * server on this machine is one they started, so restarting it is theirs to\n * do; anyone else — a hosted App, a phone — can only try again. An App's\n * reader is told the App's name: \"tablewalk\" is the database browser's, and\n * meant nothing to someone working in Compass.\n * @returns {string}\n */\nexport function unansweredSentence() {\n if (typeof navigator !== 'undefined' && navigator.onLine === false) return say('problem.offline');\n const host = typeof location === 'undefined' ? '' : location.hostname;\n if (host === 'localhost' || host === '[::1]' || host.startsWith('127.')) {\n return say('isNotAnswering', { app: appNamed() });\n }\n return say('theServerIsNotAnswering');\n}\n\n/** What an answer that is not JSON says, named by the App a reader is in. */\nexport function uninterpretableSentence(status) {\n return say('theServerAnsweredWithSomething', { status }) + say('somethingBetweenThisPage', { app: appNamed() });\n}\n\n/** The App a reader is in, by its name; in the Workbench, tablewalk. */\nconst appNamed = () => (shell().inApp() && shell().app()?.name) || 'tablewalk';\n\n/* The GET replies the server tags for revalidation (reply.ts sendRevalidated). */\nconst REVALIDATED = new Set(['/api/schema', '/api/app', '/api/pages', '/api/runtime']);\n\n/* What changes a record: a composed page's answers are forgotten when one is sent (writePaths.ts). */\n/** Requests carry the tab's App ID or DB connection ID, never a server-global selection.\n * @template {string} Path\n * @param {Path} path\n * @param {import('@mistriwork/engine/shared/resourceHttp.js').ResourceClientArgs<NoInfer<Path>>} args\n * @returns {Promise<import('@mistriwork/engine/shared/resourceHttp.js').ResourceClientReply<Path>>}\n */\nexport async function api(path, ...args) {\n return sent(path, /** @type {Record<string, unknown> | undefined} */ (args[0]), undefined);\n}\n\n/**\n * A free-form page's own request: `api`, saying which page asks\n * (`x-mistri-page`), so the server refuses a resource or a command the\n * page did not declare. Drift protection, not the boundary: the reader's\n * grants decide the request either way.\n * @param {string} page\n * @param {string} path\n * @param {Record<string, unknown>} body\n * @returns {Promise<any>}\n */\nexport async function pageApi(page, path, body) {\n return sent(path, body, { [PAGE_HEADER]: page });\n}\n\n/**\n * @param {string} path\n * @param {Record<string, unknown> | undefined} body\n * @param {Record<string, string> | undefined} headers\n * @returns {Promise<any>}\n */\nasync function sent(path, body, headers) {\n /* Counted while it is out: a navigation keeps the screen it left until\n the room it opened has had its answers (navigationHold.ts). */\n const settled = beginWork();\n const writing = isWritePath(String(path).split('?')[0], body);\n /* Its echo on the live stream is told from a change someone else made by when it was answered (ownWrites.ts). */\n const answered = writing ? ownWriteSent() : undefined;\n try {\n return await request(path, body, headers);\n } finally {\n answered?.();\n settled();\n /* Forgotten as a write is sent (request) and again as it is answered,\n before its caller refreshes the page: a compose that left while it\n was out may have read the record before the write landed. */\n if (writing) composedReads()?.forget();\n }\n}\n\n/**\n * @param {string} path\n * @param {Record<string, unknown> | undefined} body\n * @param {Record<string, string> | undefined} headers\n * @returns {Promise<any>}\n */\nasync function request(path, body, headers) {\n const parsedPath = new URL(path, window.location.href);\n if (!path.startsWith('/api/') || parsedPath.hash || parsedPath.pathname !== path.split('?')[0]) {\n throw Object.assign(new Error(say('useACanonicalApi')), { outcome: 'not_applied' });\n }\n const identity = captureSessionIdentity();\n const bootstrap = parsedPath.pathname === '/api/whoami' && identity.mode === 'unknown';\n if (!bootstrap) assertSessionCurrent(identity);\n /* A read a record page's compose already asked is answered from it\n (composedReads.ts); a write makes every such answer the past. */\n const composing = composedReads();\n const writing = isWritePath(parsedPath.pathname, body);\n if (writing) forgetFirstPaint();\n if (composing && writing) composing.forget();\n else if (composing && body && composing.may(parsedPath.pathname)) {\n const composed = await composing.reply(parsedPath.pathname, /** @type {Record<string, unknown>} */ (body));\n if (composed !== undefined) {\n assertSessionCurrent(identity);\n return composed;\n }\n }\n if (identity.tenantRequired && parsedPath.pathname !== '/api/whoami' && !parsedPath.pathname.startsWith('/api/auth/')) {\n /* The tenant's lease is the shell's (shellMode.ts): a shell that installed none has no tenant to send through. */\n const tenant = shell().tenant;\n if (!tenant) throw Object.assign(new Error(say('theTenantContextIsUnavailable')), { outcome: 'not_applied' });\n if (tenant.isMutation(parsedPath.pathname)) return await tenant.mutate(path, body, state.activeConnection);\n return await tenant.read(path, body === undefined ? undefined : { ...body }, state.activeConnection);\n }\n if (\n parsedPath.pathname === '/api/revert' &&\n body &&\n 'expectedId' in body &&\n body.expectedId !== undefined &&\n historyToken(body.expectedId, false) === undefined\n )\n throw Object.assign(new Error(say('invalidOrdinaryUndoIdentity')), { outcome: 'not_applied' });\n const { url, payload } = scopeApiRequest(path, body === undefined ? undefined : { ...body }, state.activeConnection);\n\n /* The page arrived with this answer (firstPaint.ts): taken as a reply\n would be, the account it was for held to the session as a reply's is. */\n const carried = firstPaintRead(payload ? 'POST' : 'GET', url, payload);\n if (carried) {\n if (!bootstrap) {\n assertSessionCurrent(identity);\n try {\n assertSessionCurrent(identity, carried.account ?? null);\n if (identity.mode === 'anonymous' && carried.account) throw new Error(say('theSignedInAccount'));\n } catch (error) {\n invalidateSession('account-changed');\n throw error;\n }\n }\n return carried.body;\n }\n\n let res;\n try {\n res = await fetchServer(url, {\n credentials: 'same-origin',\n /* The schema, the App, its pages and the runtime carry an ETag\n (reply.ts sendRevalidated): the browser may keep them, and asks\n again with If-None-Match every time, so a 304 answers an unchanged\n one. Everything else is never stored. */\n cache: !payload && REVALIDATED.has(parsedPath.pathname) ? 'no-cache' : 'no-store',\n method: payload ? 'POST' : 'GET',\n headers: {\n ...(payload ? { 'content-type': 'application/json' } : {}),\n ...(identity.mode === 'authenticated' ? { 'x-mistri-account': identity.accountId } : {}),\n ...headers,\n },\n body: payload ? JSON.stringify(payload) : undefined,\n });\n } catch {\n /* `fetch` rejects with \"Failed to fetch\" or \"Load failed\" depending on the\n browser, and neither says the one thing that is true and useful: the\n server this page came from is not answering. It is a local process, so\n that nearly always means it was stopped — and \"Failed to fetch\" sends\n people to look at their network. Only on this machine, though: a\n hosted App's reader, or one on a phone, cannot restart anything, and\n one who is offline needs to hear that first. */\n throw new Error(unansweredSentence());\n }\n\n if (!bootstrap) {\n assertSessionCurrent(identity);\n if (res.status === 401 && identity.mode === 'authenticated') {\n invalidateSession('expired');\n throw Object.assign(new Error(say('yourSessionExpiredReload')), { status: 401 });\n }\n try {\n assertSessionCurrent(identity, res.headers?.get('x-mistri-account'));\n if (identity.mode === 'anonymous' && res.headers?.get('x-mistri-account')) {\n throw new Error(say('theSignedInAccount'));\n }\n } catch (error) {\n invalidateSession('account-changed');\n throw error;\n }\n }\n noteSearchFallback(res);\n\n let data;\n try {\n data = await res.json();\n } catch {\n /* An answer that is not JSON is a proxy, a login page, or a crash — none\n of which the caller can do anything with, and all of which read better\n as what they are than as \"Unexpected token < in JSON\". */\n throw new Error(uninterpretableSentence(res.status));\n }\n\n if (!bootstrap) assertSessionCurrent(identity);\n /* A refused write answers its envelope — `errors` and a `rejected` outcome — at 422 when the App's rules or a\n value refused it and 409 when the database's keys did (server constraints.ts writeRefusalStatus). The form\n reads it as it reads any refused write, the field marked and the draft kept, so it is an answer here, not a\n failure. */\n if ((res.status === 409 || res.status === 422) && Array.isArray(data?.errors) && data.errors.length && data.outcome === 'rejected')\n return data;\n if (!res.ok) {\n /* The status travels with the message. Callers have always had to guess\n the kind of failure from the text of it, which is how \"No connection is\n open.\" — a 409 with a specific meaning and a specific screen to show\n for it — could only be recognised by matching a sentence. */\n const said = data.message ?? data.error ?? say('common.requestFailed', { status: res.status });\n /* `cause` is sent only to the App's author at work (server\n http/refusalReport.ts): the rule, table or column behind a refusal,\n said beside the caller's sentence wherever that sentence is drawn. */\n const failure = Object.assign(\n new Error(typeof data.cause === 'string' ? say('forTheAuthor', { message: said, cause: data.cause }) : said),\n {\n status: res.status,\n signIn: undefined,\n ...(typeof data.code === 'string' ? { code: data.code } : {}),\n ...(data.outcome === 'not_applied' || data.outcome === 'unknown' ? { outcome: data.outcome } : {}),\n /* The input a command's refusal is about (`refuse(…, { field })`): its form says the sentence beside that field. */\n ...(typeof data.field === 'string' ? { field: data.field } : {}),\n /* A busy server says when to come back (server/http/reply.ts sendBusy), in seconds: kept in ms for a read's retry (components/mount.tsx). */\n ...(res.status === 503 && data.code === 'server_busy' && Number(res.headers?.get('retry-after')) > 0\n ? { retryAfter: Number(res.headers.get('retry-after')) * 1000 }\n : {}),\n /* Where this failure's trace is, while a --dev server's observer runs:\n its badge offers \"Open in observer\" (observe/beacon.js). */\n ...(typeof data.observe === 'string' && /^(\\/[A-Za-z0-9][\\w-]*)*\\/_mistri\\/observe\\//.test(data.observe)\n ? { observe: data.observe }\n : {}),\n // The column names a refusal quotes, for a surface that says them in an App's captions.\n ...(Array.isArray(data.columns) && data.columns.every((column) => typeof column === 'string') ? { columns: data.columns } : {}),\n },\n );\n if (failure.observe) window.dispatchEvent(new CustomEvent('mistri:observe', { detail: failure.observe }));\n /* A server that signs people in answers 401 with what to draw. Told\n once, through a seam, so core does not know what a sign-in screen is. */\n /* A visitor browsing an App's public part is already in the App: a read\n past `auth.public` (a member's list or dashboard) answers 401 as\n `members_only`, and the surface that asked offers the sign-in in place\n (TableRoom's NotYours) rather than the whole page turning into it. */\n if (res.status === 401 && data.signIn) {\n failure.signIn = data.signIn;\n if (!(data.code === 'members_only' && shell().publicVisitor())) unauthorized(data.signIn);\n }\n throw failure;\n }\n fileReads(data, res.headers?.get('mistri-reads'));\n return data;\n}\n\n/**\n * Anything that has to be rebuilt when the database underneath changes.\n *\n * A connection switch is not a render: the trail is cleared, the schema is\n * replaced and every per-connection thing — layouts, saved statements, write\n * mode — is reloaded. Long-lived surfaces that are *not* inside `#content`\n * miss all of it. The SQL editor was the case that showed it: painted once\n * from the state it opened with, it went on naming the previous database\n * while the statement it ran went to the new one, and it kept offering \"Run\n * as write…\" after a switch to a connection the server would refuse.\n *\n * A set rather than the single `state.onSwitched` slot beside it, because the\n * second thing that needed to know would have silently replaced the first.\n */\nconst switchWatchers = new Set();\n\n/** Register a rebuild. Returns the way to stop listening. */\nexport function whenConnectionChanges(run) {\n switchWatchers.add(run);\n return () => switchWatchers.delete(run);\n}\n\n/**\n * Say that the connection changed. Called by the two places that switch —\n * the picker and a deep link into another database — after the new schema\n * and everything keyed to it have landed, so a watcher reads the new state\n * rather than the half-swapped one.\n */\nexport function connectionChanged() {\n /* A copy, because a watcher may well unregister itself as its last act. */\n for (const run of [...switchWatchers]) {\n try {\n run();\n } catch (err) {\n /* One surface failing to rebuild must not stop the next one. It is\n already the wrong database on screen; making it the wrong database\n everywhere is not an improvement. */\n console.error(err);\n }\n }\n}\n\n/* ---------- schema helpers ----------\n\n The shared rules, with the one schema this client holds already applied:\n the browser has one connection open and keeps its schema in `state`, so a\n caller here names a table and not a schema. Each is the server's function\n under the server's name. For a long time they were copies — the client\n could not import the TypeScript — and the copies drifted: a list limit of\n eight against ten, a date used as a row's label on one side and the key on\n the other. Now there is one rule and this is where the client reaches it. */\n\n// React roots may render once while a retired context is being unmounted.\n// An absent catalog has no records or command targets, not a fallback owner.\nexport const findTable = (id) => (state.schema?.tables ? sharedFindTable(state.schema, id) : undefined);\nexport const refsTo = (id) => referencesTo(state.schema, id);\nexport const refsFrom = (id) => referencesFrom(state.schema, id);\nexport { columnKind, labelColumn, primaryKey };\n\n/** Whether a column should be treated as a date for presentation. */\nexport const looksLikeDateColumn = (col) => Boolean(col) && looksLikeDate(col);\n\n/* The zoom-aware ruler is the ui package's (packages/ui/src/internal/ruler.ts says why\n every overlay is measured with it); the client reaches it here, under the\n names it always had. */\nexport { viewportScale, refreshViewportScale, viewportBox, viewportSize } from '@mistriwork/ui/internal/ruler.js';\n\n/** Where a row's name lives, as a view path — see labelPath in shared/schema.ts. */\nexport const labelPath = (table) => sharedLabelPath(state.schema, table, recordLabel(table));\n\n/** An explicit record name, or false when that name is unavailable to this reader. */\nexport const recordLabel = (table) => shell().recordLabel(table.id);\n\n/* ---------- paths ----------\n\n Every hop is derivable from the schema the page already holds, so proposing\n the next segment of a path is local and instant; asking the server per\n keystroke would make the one interaction that has to feel immediate the one\n that does not. The server still resolves every path for real before\n anything runs. */\n\nexport { hopName };\nexport const hopsFrom = (tableId) => sharedHopsFrom(state.schema, tableId);\n/** An empty prefix is the base; a prefix that walks nowhere is null. */\nexport const tableAtPrefix = (baseId, prefix) => sharedTableAtPrefix(state.schema, baseId, prefix) ?? null;\n\n/** How a row is named in a breadcrumb: the readable column, else the key. */\nexport function rowLabel(table, row) {\n /* The walked-to name first. A row fetched as a view carries the joined\n label under its path — `party_id.display_name` — while a plain query row\n simply lacks that key and falls through to the table's own label, then\n the primary key. One function for both, so a caller never has to know\n which kind of row it is holding. */\n const configured = recordLabel(table) !== undefined;\n for (const name of configured ? [labelPath(table)] : [labelPath(table), labelColumn(table)]) {\n const value = name != null ? row[name] : undefined;\n if (value !== null && value !== undefined && String(value) !== '') return String(value);\n }\n const values = primaryKey(table).map((c) => row[c]);\n if (configured && values.some((value) => value == null)) return '(row)';\n const pk = values.filter((value) => value != null);\n return pk.length ? pk.join('/') : '(row)';\n}\n\n/**\n * The primary key of a row, or null when this row cannot supply one.\n *\n * Null rather than a key with `undefined` in it. A projected query — `show\n * quantity`, or a child section showing four summary columns — returns rows\n * that genuinely do not carry the key, and building `{invoice_id: undefined}`\n * out of one produced a link to `~invoice_line:invoice_id=undefined` that\n * landed on \"That row is no longer there.\" Nothing had gone missing; the\n * navigation was built from a row that never had the key to begin with.\n *\n * Callers use the null to decide whether a row is clickable at all, so a row\n * that cannot be opened does not look like one that can.\n */\nexport function keyOf(table, row) {\n const columns = primaryKey(table);\n if (!columns.length) return null;\n const key = {};\n for (const col of columns) {\n if (row[col] === undefined) return null;\n key[col] = row[col];\n }\n return key;\n}\n\n/** A filter matching exactly one row by its primary key. */\nexport function keyFilter(key) {\n return { groups: [Object.entries(key).map(([column, value]) => ({ column, op: '=', value }))] };\n}\n\n/* ---------- navigation ---------- */\n\nlet navigationGeneration = 0;\nexport function invalidateNavigation() {\n navigationGeneration += 1;\n}\n\n/** Capture permission for background work without starting a new navigation intent. */\nexport function captureNavigation() {\n const generation = navigationGeneration;\n const transition = connectionGeneration();\n const connection = state.activeConnection;\n const schema = state.schema;\n const entry = currentEntry();\n return () =>\n generation === navigationGeneration &&\n transition === connectionGeneration() &&\n connection === state.activeConnection &&\n schema === state.schema &&\n entry === currentEntry();\n}\n\n/** A later intent wins even before either request has changed the visible view. */\nexport function beginNavigation() {\n invalidateNavigation();\n return captureNavigation();\n}\n\n/* A paint is a tick of its own beside the walk's: the content pane\n (components/Content.tsx) writes the address bar on every paint and leaves\n the trail's room alone on a crumb correction, which only ticks the walk.\n Flushed synchronously — `render()` has always finished painting before it\n returned, and the address gate (url.js `whileApplying`) and every caller\n that reads the page straight after depend on that. */\nlet paintTick = 0;\nexport const paintVersion = () => paintTick;\n\n/* The room is a third tick, and the one the content pane keys its room by.\n Nearly every paint starts the room over, the way a repaint always did: a\n write that repaints sees itself, another table or record is another room.\n A refinement does not. A page, a page size, or a sort or filter from a\n column header is the same table room asking a different question, and\n the room re-asks it in place (TableRoom's Query is keyed by the\n question), keeping the rows on screen until the answer lands and every\n row decoration's state with them. Only a paint made inside `refine()`\n whose table room is the one already on screen — the same connection,\n depth, table and tab — keeps it; anything else ticks. */\nlet roomTick = 0;\nlet roomOnScreen = null;\nlet refining = false;\nexport const roomVersion = () => roomTick;\n\n/** A table view's identity as a room, or null for anything else. */\nfunction tableRoomShape() {\n const top = currentEntry();\n if (!top || top.kind !== 'table') return null;\n return JSON.stringify([state.activeConnection ?? '', state.stack.length, top.table, top.tab ?? 'rows']);\n}\n\n/**\n * Paint a refinement of the table room on screen: a page, a page size, a\n * header's sort or filter. The address bar and the trail are written as for\n * any paint; the room is kept when it is still the same table room.\n */\nexport function refine(fn) {\n const was = refining;\n refining = true;\n try {\n return fn();\n } finally {\n refining = was;\n }\n}\n\n/* What happens around a paint that may start the room over: the shell\n registers the navigation hold (navigationHold.ts), which keeps the screen\n being left until the new room is complete. A seam, so core stays free of\n the DOM it holds; nothing registered, a paint is only a paint. */\nlet paintHold = null;\n/** @param {{ before(roomChanged: boolean): void; after(): void } | null} hold */\nexport function setPaintHold(hold) {\n paintHold = hold;\n}\n\nexport function render() {\n if (sessionIdentity().mode === 'locked') return;\n paintTick += 1;\n const shape = tableRoomShape();\n const roomChanged = !refining || shape === null || shape !== roomOnScreen;\n if (roomChanged) roomTick += 1;\n roomOnScreen = shape;\n paintHold?.before(roomChanged);\n flushSync(tellWalk);\n paintHold?.after();\n}\n\n/* The walk as something a component can watch. Every render and every\n crumb correction is one tick; a component that subscribes re-reads the\n stack. Kept as a counter rather than a copy of the stack, because the\n stack is mutated in place by design (a label arrives, a root resolves)\n and a copy would miss exactly those corrections. */\nconst walkListeners = new Set();\nlet walkTick = 0;\nfunction tellWalk() {\n walkTick += 1;\n for (const fn of walkListeners) fn();\n}\nexport function subscribeWalk(fn) {\n walkListeners.add(fn);\n return () => walkListeners.delete(fn);\n}\nexport const walkVersion = () => walkTick;\n\n/* Redrawing the trail alone, without re-running the view.\n A row restored from a link knows only its key, so its crumb starts as the\n key and is corrected once the row arrives. Calling render() for that would\n re-fetch the row that just resolved it — and loop. The crumbs are React\n and redraw from the walk's subscribers. */\nexport function renderCrumbs() {\n tellWalk();\n}\n\n/**\n * Move to a view and render.\n *\n * push — a step along a reference; the trail grows\n * replace — the same view, re-run (a sort, an edited filter, a tab)\n * reset — a fresh start; the trail is discarded\n *\n * The distinction is the whole meaning of the breadcrumb. A trail is a walk:\n * every crumb should be reachable from the one before it by following a key.\n * Jumping to an unrelated table is not a step along that walk, so it starts a\n * new one rather than appending browsing history.\n */\nexport function go(entry, mode = 'push') {\n if (!allowMutationExit()) return;\n invalidateNavigation();\n carryView(entry, mode);\n if (mode === 'reset') state.stack = [entry];\n /* Replacing the top of an empty stack — the landing has no entry — wrote\n to index -1: a property on the array, not a member of it. The view\n painted (currentEntry reads the same slot) and the address bar did not\n (the URL walks the array), so the landing's first refinement rendered\n with nothing in the bar to send. An empty stack gains its first entry. */\n else if (mode === 'replace' && state.stack.length) state.stack[state.stack.length - 1] = entry;\n else state.stack.push(entry);\n if (mode !== 'replace') state.page = 0;\n /* How the address bar should record this. A step in the walk earns a\n history entry so the browser's Back button undoes it; a refinement of the\n view you are already on does not, or Back would spend three presses\n undoing sort toggles. */\n state.navMode = mode === 'replace' ? 'replace' : 'push';\n render();\n}\n\n/**\n * Which App view opened a room rides the walk as `view` — its key — and\n * appnav.js reads it where two views share a table. The callers that\n * refine a list (a sort, a header menu, a tab) or open one of its records\n * build a plain `tableView`/`rowView` and know nothing of views, so the\n * walk carries it for them: a same-table refinement in place, and a record\n * of that table opened from it. A caller that names a view — or names\n * none, `view: undefined` — is taken at its word; a fresh start carries\n * nothing.\n */\nfunction carryView(entry, mode) {\n const top = state.stack[state.stack.length - 1];\n if (mode === 'reset' || !top?.view || !entry || 'view' in entry || entry.table !== top.table) return;\n if (entry.kind === 'row' || (entry.kind === 'table' && mode === 'replace')) entry.view = top.view;\n}\n\nexport function backTo(index) {\n if (!allowMutationExit()) return;\n invalidateNavigation();\n state.stack.length = index + 1;\n state.page = 0;\n // Clicking a crumb is a navigation, so Back returns you to where you were.\n state.navMode = 'push';\n render();\n}\n\n/**\n * A table view, optionally filtered. The query text is what the bar shows.\n *\n * @param {*} tableId\n * @param {*} [query]\n * @param {*} [tab]\n */\nexport function tableView(tableId, query, tab) {\n return { kind: 'table', table: tableId, query: query ?? tableId, tab: tab ?? 'rows' };\n}\n\n/**\n * A breakdown: counts and sums, grouped by something.\n *\n * `table` is the base being counted, so the sidebar highlights it and the\n * crumb reads like a table's. It is deliberately not a `tab` of the table\n * view: the rows of a breakdown are groups rather than records, and a tab\n * would promise they behave like the rows beside them.\n */\nexport function breakdownView(tableId, query) {\n return { kind: 'breakdown', table: tableId, query };\n}\n\n/** A single row. The key is enough to re-fetch it, so nothing goes stale. */\nexport function rowView(tableId, key, label) {\n return { kind: 'row', table: tableId, key, label };\n}\n\n/**\n * A page, with or without a root yet.\n *\n * `table` is the page's base rather than a table being browsed, so that the\n * sidebar highlights the right thing and a page can find its way back to a\n * suggested layout when the id names no configured page.\n *\n * @param {*} pageId\n * @param {*} [tableId]\n * @param {*} [key]\n * @param {*} [label]\n */\nexport function pageView(pageId, tableId, key, label) {\n return { kind: 'page', pageId, table: tableId, key, label };\n}\n\nexport const currentEntry = () => state.stack[state.stack.length - 1];\n\n// Registered by the draft store: core must not import the editing/rendering tree.\nlet recordMutationPending = () => false;\n/** @param {() => boolean} probe */\nexport function setRecordMutationPendingProbe(probe) {\n recordMutationPending = probe;\n}\n// Transient editors supplement the draft/tenant guards; they never settle an\n// operation merely because its dialog unmounted or the URL did not change.\nconst surfaceMutationProbes = new Set();\n/** @param {() => boolean} probe */\nexport function registerSurfaceMutationPendingProbe(probe) {\n surfaceMutationProbes.add(probe);\n return () => {\n surfaceMutationProbes.delete(probe);\n };\n}\n\n/** Voluntary navigation cannot discard a tenant operation's uncertain result.\n * Forced session retirement bypasses this guard and still clears private data. */\nexport function allowMutationExit() {\n let pending = true;\n try {\n pending = recordMutationPending();\n } catch {\n /* An unreadable owner cannot authorize an exit. */\n }\n if (pending) {\n toast(say('waitForThePending'), 'error');\n return false;\n }\n for (const probe of surfaceMutationProbes) {\n let blocked = true;\n try {\n blocked = probe();\n } catch {\n /* An unreadable surface cannot authorize exit. */\n }\n if (blocked) {\n toast(say('aChangeIsPending'), 'error');\n return false;\n }\n }\n if (!shell().mutations.hasUnresolved() && !shell().accountBusy()) return true;\n toast(say('aChangeIsPending'), 'error');\n return false;\n}\n\n/* How a record opens, when the walk is already looking at pages.\n *\n * A page is a way of reading a record, not a place: someone who chose the\n * page view for a customer wants the invoice they clicked to open the same\n * way, and dropping them into the plain record view — and making them press\n * \"As a page\" again — is the walk changing shape underneath them.\n *\n * The resolver is the shell's (shellMode.ts) rather than imported because\n * core must not know about pages: `page.js` already imports this file, and\n * the preference for a saved or configured page over the suggested one lives\n * there with the rest of the page machinery. No resolver means nothing changes.\n */\n\n/** The page a table's records read through here, or null when unavailable or\n cancelled. Current lookup failures reject; cancelled failures are ignored.\n For a deep link\n that arrives as a row while an app is on: the walk would have opened it\n as a page, and a pasted link should land where a click would. */\nexport async function pageFor(tableId) {\n const pageResolver = shell().resolvePage;\n if (!pageResolver || !shell().pageAffinity()) return null;\n const isCurrent = captureNavigation();\n try {\n const pageId = await pageResolver(tableId);\n return isCurrent() ? (pageId ?? null) : null;\n } catch (error) {\n if (!isCurrent()) return null;\n throw error;\n }\n}\n\n/* Whether pages are the walk's default, not just its current mode.\n *\n * The shell's answer for the same reason the resolver is: an app (appnav.js)\n * reads records through pages by design — that is what makes it an app\n * rather than a browser — but core must not know apps exist. The\n * Workbench's answer is no, and records open the way they always did. */\n\n/** Whether records open as pages here — the sign that an app is reading. */\nexport function prefersPages() {\n return Boolean(shell().pageAffinity());\n}\n\n/* ---------- who is asked to sign in ---------- */\n\nlet unauthorizedShown = false;\n\n/** What happens when the server demands a sign-in: the shell's answer (shellStart.js), once per page load. */\nfunction unauthorized(info) {\n if (unauthorizedShown) return;\n unauthorizedShown = true;\n shell().unauthorized(info);\n}\n\n/* ---------- what may be written ---------- */\n\n/**\n * Whether a table may be written from here, beyond the connection's own\n * flag. In the Workbench, everything the connection allows; an app answers\n * its scope — the tables its manifest names, or nothing — and every write\n * affordance asks before it appears. The server enforces the same list;\n * this is what keeps a button from promising a write the server refuses.\n */\nexport function writeAllowed(tableId) {\n return Boolean(shell().writeScope(tableId));\n}\n\n/* ---------- the empty word ---------- */\n\n/**\n * What an empty value reads as. In the workbench, plain DB mode, it is\n * `null`, because that is the truth about the row and the reader is reading\n * the database. An app answers a dash: a record form full of the word `null`\n * is the schema leaking through, and \"nothing here\" is a form's own word for\n * it, with the truth one hover away. The shell's answer, so core never asks\n * whether an app is on.\n */\nexport function emptyWord() {\n return shell().emptyWord() ?? 'null';\n}\n\n/* ---------- column labels ---------- */\n\n/**\n * The one place a column's shown name is decided, answered by whoever owns\n * the words — app mode answers the manifest's labels plus the humanized\n * fallback; the workbench, plain DB mode, shows the raw name on every\n * surface. One seam instead of a per-surface `inAppMode()` map, because\n * the surfaces that forgot the map were exactly the ones still showing\n * INVOICE_ID in an app.\n */\n\n/**\n * What to call a column of a table right now, or null for \"the raw name\".\n * `column` may be a dotted label path — \"account_id.name\" — and the\n * source decides how that reads. `contextPath` names a related record\n * already identified by the containing field section.\n */\nexport function columnLabel(tableId, column, contextPath = '') {\n return shell().label(tableId, column, contextPath) ?? null;\n}\n\n/**\n * How a column's values dress — the label seam's twin, for rendering.\n * App mode answers the manifest's display rules; in the workbench every\n * value keeps the explorer's plain face. Returns a rule\n * ({ format?, currency?, tone?, bar? }) or null for \"as it is\".\n * @returns {any}\n */\nexport function columnDisplay(tableId, column) {\n return shell().display(tableId, column) ?? null;\n}\n\n/**\n * Open one record, in whichever view the walk is already in.\n *\n * Every route into a record goes through here, so the mode is sticky by\n * default and a call site that wants the record view specifically still says\n * `go(rowView(...))` — which is what the table grid and the record view's own\n * related lists do, since neither is in page view to begin with.\n */\n/* How a record opens can be the app's decision — a drawer, one day a\n modal — the shell's answer, as page affinity is: core stays app-ignorant\n and asks. An opener that answers true has handled the click. Who begins\n reading a record's page ahead of the press that opens it\n (recordCompose.ts) is the shell's answer too. */\n\n/**\n * Begin reading the page a press on this record will open, before the\n * press is over: the same page `openRecord` would choose, asked of the\n * prefetcher, which decides whether the record opens as a page at all.\n * Nothing moves and nothing is drawn; a record that opens some other way\n * asks nothing.\n */\nexport function prefetchRecord(tableId, key) {\n const { prefetchRecord: recordPrefetcher, resolvePage: pageResolver, pageAffinity } = shell();\n if (!recordPrefetcher || !pageResolver || !tableId || !key || typeof key !== 'object') return;\n const here = currentEntry();\n if (here?.kind === 'page' && !here.key && here.table === tableId) {\n recordPrefetcher(here.pageId, tableId, key);\n return;\n }\n if (!(here?.kind === 'page' || pageAffinity(tableId))) return;\n void Promise.resolve(pageResolver(tableId)).then(\n (pageId) => {\n if (pageId) recordPrefetcher(pageId, tableId, key);\n },\n () => {},\n );\n}\n\n/* A glance at one record without leaving the list (Space on a row), which\n peekpanel.js draws: the shell's answer, as the opener is, so core and the\n grid stay free of the drawer. */\n/** Glance at a record over the list; false where the shell draws no drawer. */\nexport function peekRecord(tableId, key, label, records, list) {\n const recordPeeker = shell().peekRecord;\n if (!recordPeeker) return false;\n const branch = unionBranch(tableId, key);\n if (branch) recordPeeker(branch.table, branch.key, label);\n else if (branch === undefined) recordPeeker(tableId, key, label, records, list);\n return true;\n}\n\n/** A union row's own record — `{ table, key }`, null where its branch is not this reader's — or undefined for any other table's. */\nfunction unionBranch(tableId, key) {\n const branches = findTable(tableId)?.union?.branches;\n const branch = branches?.find((b) => b.from === key?.from);\n return branches && (branch ? { table: branch.from, key: { [branch.key]: key.key } } : null);\n}\n\nexport async function openRecord(tableId, key, label, how = 'push', opts = {}) {\n /* A union's row is its branch's record (appUnions.ts): opened there, without the union's list to step through. */\n const branch = unionBranch(tableId, key);\n if (branch !== undefined) return branch ? openRecord(branch.table, branch.key, label, how, { direct: opts.direct }) : undefined;\n if (!allowMutationExit()) return;\n const isCurrent = beginNavigation();\n /* `direct` is the drawer's own \"Details →\" declining its own answer:\n without it the opener would answer the click it just handed over. */\n /* A registered opener may either consume the click (`true`) or ask for a\n row route (`'row'`) while still taking part in the normal navigation\n fencing. The latter is used by app-owned split workspaces: their detail\n pane is a row surface, but automatic page affinity must not replace it\n with a configured page. */\n const answer = !opts.direct ? shell().openRecord(tableId, key, label, opts.records, opts.list) : false;\n /* `{ under }` is a row route that first stands a list beneath it: a split\n picked at the App's root, where the walk is empty (appnav.js). */\n const under = answer && typeof answer === 'object' ? answer.under : undefined;\n const opening = under ? 'row' : answer;\n /* The list it was opened from rides the record's entry, so the record can\n step to its neighbors (recordTrail.ts). Not in the address: a link is to\n the record, and the list it was reached from is this reader's walk. */\n const trail = trailFor(tableId, key, opts.records, opts.list);\n const trailed = (entry) => (trail ? { ...entry, trail } : entry);\n if (opening === true) return;\n /* Rooting the page you are standing on. A rootless page shows its list\n as the chooser, and choosing is not a step of the walk — the entry\n gains its key in place, or the trail reads `invoice › invoice ›\n invoice`, one hollow crumb per change of mind. */\n const here = currentEntry();\n if (here?.kind === 'page' && !here.key && here.table === tableId) {\n go({ ...here, key, label, rootBody: undefined }, 'replace');\n return;\n }\n const pageResolver = shell().resolvePage;\n if (opening !== 'row' && (here?.kind === 'page' || shell().pageAffinity(tableId)) && pageResolver) {\n try {\n const pageId = await pageResolver(tableId);\n if (!isCurrent()) return;\n if (pageId) {\n go(trailed(pageView(pageId, tableId, key, label)), how);\n return;\n }\n } catch {\n /* A page that cannot be resolved is not a reason to go nowhere. */\n }\n }\n if (!isCurrent()) return;\n /* A split's pick is its list's selection, not a step. The first pick\n stands the row on its list — the split's address is the list and then\n the key, `/thread/inbox/~2`, so the list entry stays under it —\n and every later pick replaces that row, as a rooted page gains its key in\n place above: pushed, each pick was one more crumb and one more Back\n (Crumbs.tsx draws the list and its row as one crumb). Only the split's\n own room: a record opened from anywhere else pushes. */\n const picked = opening === 'row' && here?.kind === 'row' && here.table === tableId;\n if (under && !state.stack.length) state.stack.push(under);\n go(trailed(rowView(tableId, key, label)), picked ? 'replace' : how);\n}\n\n/* ---------- chrome shared between views ---------- */\n\n/* Whether the compiled SQL is on show. Held here rather than read off a\n button, because the control that sets it moved into a menu and the pane\n should not care which widget last toggled it. */\nlet explaining = false;\n\nexport const explainVisible = () => explaining;\n\nexport function setExplaining(on) {\n explaining = on;\n showExplain();\n}\n\nexport function setExplain(sql) {\n $('explain').textContent = sql ?? '';\n lastExplained = sql ?? '';\n showExplain();\n}\n\n/* The pane is hidden as a whole rather than the <pre> alone, so the control\n that edits the statement cannot outlive the statement it edits. */\nfunction showExplain() {\n const pane = $('explain-pane');\n if (pane) pane.hidden = !explaining || !$('explain').textContent;\n}\n\n/** The statement the current view compiled to, for anything that wants to run it. */\nlet lastExplained = '';\nexport const explainedSql = () => lastExplained;\n\nexport function showErrors(errors) {\n const box = $('errors');\n box.replaceChildren(...errors.map((e) => el('p', { text: e.message })));\n box.hidden = !errors.length;\n\n /* A query that did not run must not be saveable as a filter.\n \n The banner said `\"quantitee\" is not a column on invoice_line`, correctly\n and helpfully — and beside it \"+ Save this filter\" stayed live, so a typo\n could be given a name and kept. The clause chips stayed ordinary too,\n which is the deeper misdescription: when the query fails *none* of them\n are in effect, so a row of chips reading as the current filter is\n describing something that is not happening.\n\n Done here rather than where the bar is built because this is the only\n moment the app knows: the bar is drawn from the query text, before the\n answer comes back. */\n const failed = errors.length > 0;\n for (const save of shellQueryAll('.view-save')) {\n if (!(save instanceof HTMLButtonElement)) continue;\n save.disabled = failed;\n save.title = failed ? say('fixTheQueryBefore') : '';\n }\n shellQuery('.views')?.classList.toggle('not-in-effect', failed);\n}\n\n/**\n * Quote a value for the query bar, only when it needs it.\n *\n * The one place the client writes a value into the grammar, and it is the\n * server's own `quoteValue`, not a copy: the copy's rule (\"word characters,\n * dots and hyphens are safe\") let a slug carrying `select` through bare, and\n * the parser read it as a clause. `quoteValue` asks the parser whether the\n * bare value reads back as itself, so the two cannot disagree again.\n */\nexport function quote(value) {\n return quoteValue(value);\n}\n\n/** A short-lived message at the top of the content area. */\n/**\n * A passing message, optionally with one thing to do about it.\n *\n * The action exists for the case where the message *is* the moment to act —\n * \"Updated 2 columns\" is exactly when someone realises it was the wrong row,\n * and sending them to a menu to find Undo wastes the seconds in which they\n * still remember what they did. Anything that needs more than one verb belongs\n * in a dialog rather than here.\n *\n * @param {*} message\n * @param {string} [tone]\n * @param {*} [action]\n */\nexport function toast(message, tone = 'info', action) {\n const host = $('toast');\n if (!host || sessionIdentity().mode === 'locked') return;\n showToast(host, message, tone, action, prefersPages());\n}\n\n/* ---------- persistence ----------\n\n Every localStorage call is wrapped. It throws in Safari's private mode, it\n throws when the quota is exceeded, and it is absent entirely in some\n embedded webviews. A browser that cannot remember your saved filters should\n still let you browse the database — losing a preference is not a reason to\n lose the tool. */\n\nexport function loadJson(key, fallback) {\n try {\n const raw = captureScopedStorage(() => window.localStorage).getItem(key);\n if (!raw) return fallback;\n const parsed = JSON.parse(raw);\n return parsed ?? fallback;\n } catch {\n // Corrupt or unreadable: start fresh rather than breaking the page.\n return fallback;\n }\n}\n\nexport function saveJson(key, value) {\n try {\n captureScopedStorage(() => window.localStorage).setItem(key, JSON.stringify(value));\n return true;\n } catch {\n return false;\n }\n}\n"],"mappings":"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"}