@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-C-J32eBn.js","names":["say","say","findTable","resourceOf"],"sources":["../../../engine/src/shared/tableSizing.ts","../../../engine/src/shared/formValues.ts","../../../engine/src/shared/appIcons.ts","../../../engine/src/shared/relatedCreate.ts","../../src/shared/appAddress.ts","../../src/shared/appModel.ts","../../src/shared/command.ts","../../../engine/src/shared/computedParse.ts","../../../engine/src/shared/funnel.ts","../../../engine/src/shared/pageLayouts.ts","../../../kit/src/client/recordCompose.ts","../../../kit/src/client/copy/record/components/recordSurface.ts","../../../kit/src/client/components/recordSurface.ts","../../../engine/src/shared/appComponents.ts","../../../engine/src/shared/pageConfig.ts","../../src/shared/formRules.ts","../../src/shared/appFlows.ts","../../src/shared/appDashSections.ts","../../src/shared/appViews.ts","../../src/client/copy/components/components/sdk/blocks.ts","../../src/client/components/sdk/blocks.tsx"],"sourcesContent":["export type TableSizing = 'fit' | 'scroll';\nexport const TABLE_SIZINGS: readonly TableSizing[] = ['fit', 'scroll'];\n\n/** Omission preserves the renderer's fit default without rewriting authored config. */\nexport function parseTableSizing(value: unknown, at: string): TableSizing | undefined {\n if (value === undefined) return undefined;\n if (value !== 'fit' && value !== 'scroll') throw new Error(`${at} has an invalid \"sizing\". Use \"fit\" or \"scroll\".`);\n return value;\n}\n","/**\n * How a form reads a record's own values, in the browser as someone types and on the server after: a condition in\n * the query language compiled to comparisons and judged against a row, a scalar a declaration may hold, the words a\n * default fills in (`today`, `now`, `me`), the starting values a schema's DEFAULTs already say, a stored value as an\n * expression reads it, and a multi-step form's step. The engine's, because the kit's New form draws a table's form\n * with them in both shells; the App framework's form declarations — their parsing, validation, child groups and\n * rules — are formRules.ts and formLines.ts, which re-export what they always named.\n */\nimport { columnKind, isFlagColumn, type ColumnMeta, type DatabaseSchema, type TableMeta } from './schema.js';\nimport type { ComputedValue } from './computed.js';\nimport { NO_ONE_HERE, parseQuery } from './query.js';\nimport type { Condition } from './filter.js';\n\n/** Data only: local columns, no paths, expressions, clock or executable hooks. */\nexport type FormScalar = string | number | boolean | null;\n/** The comparisons a compiled condition holds: the query language's own, named for the evaluator. */\nexport type FormOperator = 'eq' | 'ne' | 'lt' | 'lte' | 'gt' | 'gte';\n/**\n * A condition in the query language — the one a page section's `when` and an\n * action's `when` speak — read against a record's own values rather than run\n * as SQL: \"completed = false\", \"stage = closed_won and probability < 100\".\n * One language for every condition an App writes; what a form or a rule can\n * evaluate in memory is its comparisons of local columns (`=`, `!=`, `<`,\n * `<=`, `>`, `>=`, `in`, `is (not) empty`) under `and`, `or` and brackets.\n */\nexport type FormCondition = string;\n/** One compiled comparison: true when the field compares with any of the values (one, or an `in` list), or with\n * the record's own `other` column. */\ninterface Comparison {\n field: string;\n operator: FormOperator;\n values: readonly FormScalar[];\n other?: string;\n}\n/** A compiled condition: OR of ANDs, the query language's own filter shape. */\nexport type CompiledCondition = readonly (readonly Comparison[])[];\n\n/** A finite, data-only step in a multi-step form. Fields belong to one step. The\n * parser gives a step without an id its label's slug, as `step()` does. */\nexport interface FormStep<Field extends string = string> {\n id: string;\n /** The short name: the review's section, the stepper's entry and the progress bar's \"Step 2 of 5: Income\". */\n label: string;\n /** Every field the step asks, in order, rows read through. */\n fields: Field[];\n /** The fields as rows, where any row holds more than one (at most three side by side, stacking on a narrow\n screen); absent, one field to a row. Authored as an inner list: `[['first_name', 'last_name'], 'dob']`. */\n rows?: Field[][];\n /** The step's heading in the intake style (the room's h1); absent, its label. */\n title?: string;\n /** One sentence under the title. */\n lead?: string;\n /** The primary button's words on this step; absent, \"Continue\". */\n next?: string;\n /** The primary's words while every field of the step is empty (each optional): \"Skip\". */\n skip?: string;\n}\n\n/** How a multi-step form is drawn, both under one progress bar: `stepper` (the step's name over its fields) or `intake`\n (the step's title large, one question at a time, a sticky bar of actions and an editable review). */\nexport type FormStyle = 'stepper' | 'intake';\n\n/** Comparisons in one condition, counted after `or` is multiplied out. */\nexport const MAX_FORM_PREDICATES = 16;\n/** The longest condition text. */\nexport const MAX_FORM_CONDITION = 2000;\nconst MAX_FORM_TEXT = 4096;\n// eslint-disable-next-line no-control-regex -- Declaration names must exclude ASCII control characters.\nexport const LOCAL_FIELD = /^[^\\s.\\x00-\\x1f\\x7f]{1,256}$/u;\nexport const RESERVED = new Set(['__proto__', 'prototype', 'constructor']);\n\nfunction fail(where: string): never {\n // Never stringify rejected data: it may contain row values or executable hooks.\n throw new Error(`${where}: invalid form declaration.`);\n}\n\nexport function scalar(raw: unknown, where: string): FormScalar {\n if (raw === null) return null;\n if (typeof raw === 'boolean') return raw;\n if (typeof raw === 'string' && raw.length <= MAX_FORM_TEXT) return raw;\n if (typeof raw === 'number' && Number.isFinite(raw) && Math.abs(raw) <= Number.MAX_SAFE_INTEGER) return raw;\n return fail(where);\n}\n\nfunction frozen<T extends object>(value: T): T {\n return Object.freeze(value);\n}\n\nconst QUERY_OPERATORS: Readonly<Record<string, FormOperator>> = { '=': 'eq', '!=': 'ne', '<': 'lt', '<=': 'lte', '>': 'gt', '>=': 'gte' };\n\n/* The one table a condition is read against, for the parser: with the\n record's columns it types each literal by its column (\"02134\" stays text\n beside a zip code) and names a mistyped column; without, it judges the\n words alone, which is what an App's parse can know before a catalog. */\nfunction conditionSchema(name: string, columns: readonly ColumnMeta[]): DatabaseSchema {\n return {\n label: '',\n dialect: 'sqlite',\n readAt: '',\n foreignKeys: [],\n tables: [{ name, schema: '', id: name, isView: false, columns: [...columns] }],\n };\n}\n\n/* A validation reads its own columns and, through one reference, the record that reference names\n (`order_id.status`): the rule a child keeps about its parent (\"parts only while the order is on the bench\"),\n judged on the server with the parent read in the write's own transaction. One hop: a longer path is a rule\n about a record two tables away, which a command reading it states more plainly. */\nfunction readsThroughReference(column: string): boolean {\n const [via, name, ...more] = column.split('.');\n return !more.length && name !== undefined && [via, name].every((part) => LOCAL_FIELD.test(part) && !RESERVED.has(part));\n}\n\nfunction comparison(condition: Condition, where: string, paths: boolean): Comparison {\n if (!LOCAL_FIELD.test(condition.column) || RESERVED.has(condition.column)) {\n if (!paths || !readsThroughReference(condition.column))\n throw new Error(\n paths\n ? `${where}: \"${condition.column}\" goes past one reference; a validation reads the record's own columns and, through one reference, that record's (order_id.status).`\n : `${where}: \"${condition.column}\" is a path; a condition here reads the record's own columns.`,\n );\n }\n const values = (v: unknown): FormScalar[] => (Array.isArray(v) ? v : [v]).map((item) => scalar(item, where));\n if (condition.other !== undefined) {\n const op = QUERY_OPERATORS[condition.op];\n if (!op || !LOCAL_FIELD.test(condition.other) || RESERVED.has(condition.other)) fail(where);\n return { field: condition.column, operator: op, values: [], other: condition.other };\n }\n if (condition.op === 'isNull') return { field: condition.column, operator: 'eq', values: [null] };\n if (condition.op === 'isNotNull') return { field: condition.column, operator: 'ne', values: [null] };\n if (condition.op === 'in') return { field: condition.column, operator: 'eq', values: values(condition.value) };\n const op = QUERY_OPERATORS[condition.op];\n if (!op) {\n throw new Error(\n `${where}: \"${condition.op}\" matches text, which a condition here cannot; compare with =, !=, <, <=, >, >=, in or is empty.`,\n );\n }\n return { field: condition.column, operator: op, values: values(condition.value) };\n}\n\n/** A condition's text, compiled through the query language's own parser. `columns`, when known, type its literals. */\nexport function compileCondition(\n raw: unknown,\n where: string,\n columns?: readonly ColumnMeta[],\n table = 'record',\n paths = false,\n): CompiledCondition {\n if (typeof raw !== 'string' || !raw.trim() || raw.length > MAX_FORM_CONDITION) {\n throw new Error(`${where}: a condition is query-language text, like \"status = shipped\".`);\n }\n // Against the record's columns a bare word beside a number, date or flag may name another column.\n const parsed = parseQuery(`${table} ${raw}`, columns ? { schema: conditionSchema(table, columns), columnValues: true } : {});\n /* A form's condition and a validation are judged against the record's own values, in the browser as someone\n types and on the server after: no session is either place, so `me` is nobody's here (appMe.ts checkMe). */\n if (parsed.errors[0]?.message === NO_ONE_HERE)\n throw new Error(\n `${where} compares with me. A form's conditions and a validation read the record's own values, where the session is not; ` +\n 'show the field always, or narrow who may write it with roles.',\n );\n if (parsed.errors.length) throw new Error(`${where}: ${parsed.errors[0].message}`);\n const query = parsed.query!;\n if (query.orderBy.length || query.columns.length || query.limit !== undefined) {\n throw new Error(`${where}: a condition is only a condition — no sort, show or limit.`);\n }\n if (!query.filter.groups.length) throw new Error(`${where}: a condition is query-language text, like \"status = shipped\".`);\n const groups = query.filter.groups.map((group) => frozen(group.map((c) => frozen(comparison(c, where, paths)))));\n if (groups.flat().length > MAX_FORM_PREDICATES) throw new Error(`${where}: a condition compares at most ${MAX_FORM_PREDICATES} times.`);\n return frozen(groups);\n}\n\ntype Family = 'number' | 'boolean' | 'text' | 'date' | 'datetime' | 'instant';\nexport function family(column: ColumnMeta): Family | undefined {\n const type = column.type.trim().toLowerCase();\n if (/^(?:bool|boolean)$/.test(type)) return 'boolean';\n if (\n /^(?:(?:tiny|small|medium|big)?int(?:eger)?|int[248]|(?:small|big)?serial|numeric|decimal|real|double(?: precision)?|float[48]?)(?:\\s*\\(\\s*\\d+(?:\\s*,\\s*\\d+)?\\s*\\))?(?: unsigned)?$/.test(\n type,\n )\n )\n return 'number';\n if (/^(?:date)$/.test(type)) return 'date';\n if (/^(?:timestamptz(?:\\(\\d+\\))?|timestamp(?:\\(\\d+\\))? with time zone)$/.test(type)) return 'instant';\n if (/^(?:datetime|timestamp)(?:\\(\\d+\\))?(?: without time zone)?$/.test(type)) return 'datetime';\n if (\n column.allowed ||\n /^(?:text|(?:tiny|medium|long)text|n?varchar|(?:character varying)|n?char|character|uuid|clob)(?:\\(\\d+\\))?$/.test(type) ||\n type.startsWith('enum(')\n )\n return 'text';\n return undefined;\n}\n\ninterface Decimal {\n coefficient: bigint;\n scale: number;\n}\ntype Normalized =\n | { kind: 'null' }\n | { kind: 'number'; value: Decimal }\n | { kind: 'boolean'; value: boolean }\n | { kind: 'text' | 'date' | 'datetime' | 'instant'; value: string };\n\nfunction decimal(raw: unknown): Decimal | undefined {\n if (typeof raw === 'bigint') raw = raw.toString();\n if (typeof raw === 'number') {\n if (!Number.isFinite(raw) || Math.abs(raw) > Number.MAX_SAFE_INTEGER) return undefined;\n raw = String(raw);\n }\n if (typeof raw !== 'string' || raw.length > MAX_FORM_TEXT) return undefined;\n const match = /^([+-]?)(?:(\\d+)(?:\\.(\\d*))?|\\.(\\d+))(?:[eE]([+-]?\\d{1,4}))?$/.exec(raw.trim());\n if (!match) return undefined;\n const exponent = Number(match[5] ?? 0);\n if (Math.abs(exponent) > MAX_FORM_TEXT) return undefined;\n const fraction = match[3] ?? match[4] ?? '';\n let digits = ((match[2] ?? '0') + fraction).replace(/^0+/, '') || '0';\n let scale = fraction.length - exponent;\n while (digits.endsWith('0') && digits !== '0') {\n digits = digits.slice(0, -1);\n scale--;\n }\n return { coefficient: BigInt((match[1] === '-' ? '-' : '') + digits), scale: digits === '0' ? 0 : scale };\n}\n\nfunction validDate(value: string): boolean {\n const match = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(value);\n if (!match) return false;\n const [, y, m, d] = match;\n const year = Number(y),\n month = Number(m),\n day = Number(d);\n const days = [31, year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];\n return year >= 1 && month >= 1 && month <= 12 && day >= 1 && day <= days[month - 1];\n}\n\nfunction dateValue(raw: unknown, kind: 'date' | 'datetime' | 'instant'): string | undefined {\n // Date instances are accepted only for instants; interpreting local dates using\n // the machine's timezone would make validation depend on deployment settings.\n if (raw instanceof Date && kind === 'instant') {\n const time = Date.prototype.getTime.call(raw);\n if (!Number.isFinite(time)) return undefined;\n raw = new Date(time).toISOString();\n }\n if (typeof raw !== 'string' || raw.length > 64) return undefined;\n if (kind === 'date') return validDate(raw) ? raw : undefined;\n const match = /^(\\d{4}-\\d{2}-\\d{2})[T ](\\d{2}):(\\d{2})(?::(\\d{2})(?:\\.(\\d{1,9}))?)?(Z|[+-]\\d{2}(?::?\\d{2})?)?$/.exec(raw);\n if (!match || !validDate(match[1]) || Number(match[2]) > 23 || Number(match[3]) > 59 || Number(match[4] ?? 0) > 59) return undefined;\n const [, date, hour, minute, seconds = '00', fraction = '', zone] = match;\n if (kind === 'datetime') return zone ? undefined : `${date}T${hour}:${minute}:${seconds}.${fraction.padEnd(9, '0')}`;\n if (!zone) return undefined;\n let offset = 0;\n if (zone !== 'Z') {\n const parts = /^([+-])(\\d{2})(?::?(\\d{2}))?$/.exec(zone)!;\n const hours = Number(parts[2]),\n minutes = Number(parts[3] ?? 0);\n if (hours > 14 || minutes > 59 || (hours === 14 && minutes !== 0)) return undefined;\n offset = (hours * 60 + minutes) * (parts[1] === '-' ? -1 : 1);\n }\n const time = Date.parse(`${date}T${hour}:${minute}:${seconds}Z`) - offset * 60_000;\n // Store epoch nanoseconds as decimal text; comparison converts only this exact\n // internally produced integer, never the original DB identifier or timestamp.\n return String(BigInt(time) * 1_000_000n + BigInt(fraction.padEnd(9, '0')));\n}\n\nexport function normalize(column: ColumnMeta, raw: unknown): Normalized | undefined {\n const kind = family(column);\n if (!kind) return undefined;\n if (raw === null) return { kind: 'null' };\n if (kind === 'number') {\n const value = decimal(raw);\n if (!value || (/int|serial/i.test(column.type) && value.scale > 0)) return undefined;\n return { kind, value };\n }\n if (kind === 'boolean') {\n if (raw === true || raw === 1 || raw === 1n || (typeof raw === 'string' && /^(true|yes|1|t)$/i.test(raw.trim())))\n return { kind, value: true };\n if (raw === false || raw === 0 || raw === 0n || (typeof raw === 'string' && /^(false|no|0|f)$/i.test(raw.trim())))\n return { kind, value: false };\n return undefined;\n }\n if (kind === 'text') return typeof raw === 'string' && raw.length <= MAX_FORM_TEXT ? { kind, value: raw } : undefined;\n const value = dateValue(raw, kind);\n return value === undefined ? undefined : { kind, value };\n}\n\nfunction compare(left: Normalized | undefined, op: FormOperator, right: Normalized | undefined): boolean | 'unknown' {\n if (!left || !right) return 'unknown';\n if (left.kind === 'null' || right.kind === 'null') {\n if (op !== 'eq' && op !== 'ne') return 'unknown';\n const equal = left.kind === right.kind;\n return op === 'eq' ? equal : !equal;\n }\n if (left.kind !== right.kind) return 'unknown';\n let order: number;\n if (left.kind === 'number' && right.kind === 'number') {\n const scale = Math.max(left.value.scale, right.value.scale);\n const a = left.value.coefficient * 10n ** BigInt(scale - left.value.scale);\n const b = right.value.coefficient * 10n ** BigInt(scale - right.value.scale);\n order = a < b ? -1 : a > b ? 1 : 0;\n } else if (left.kind === 'instant' && right.kind === 'instant') {\n const a = BigInt(left.value),\n b = BigInt(right.value);\n order = a < b ? -1 : a > b ? 1 : 0;\n } else if (left.kind === 'boolean' && right.kind === 'boolean') {\n if (op !== 'eq' && op !== 'ne') return 'unknown';\n order = left.value === right.value ? 0 : 1;\n } else if (typeof left.value === 'string' && typeof right.value === 'string') {\n // Text ordering is deliberately unavailable: DB collations are not portable.\n if (left.kind === 'text' && op !== 'eq' && op !== 'ne') return 'unknown';\n order = left.value < right.value ? -1 : left.value > right.value ? 1 : 0;\n } else return 'unknown';\n switch (op) {\n case 'eq':\n return order === 0;\n case 'ne':\n return order !== 0;\n case 'lt':\n return order < 0;\n case 'lte':\n return order <= 0;\n case 'gt':\n return order > 0;\n case 'gte':\n return order >= 0;\n }\n}\n\nexport function columnNamed(columns: readonly ColumnMeta[], name: string): ColumnMeta | undefined {\n const matches = columns.filter((c) => c.name === name);\n return matches.length === 1 ? matches[0] : undefined;\n}\n\nexport function rowValue(row: Readonly<Record<string, unknown>>, name: string): unknown {\n const descriptor = Object.getOwnPropertyDescriptor(row, name);\n return descriptor && Object.hasOwn(descriptor, 'value') ? descriptor.value : undefined;\n}\n\nexport function comparisonResult(\n p: Comparison,\n row: Readonly<Record<string, unknown>>,\n columns: readonly ColumnMeta[],\n): boolean | 'unknown' {\n const column = columnNamed(columns, p.field);\n if (!column) return 'unknown';\n if (p.other !== undefined) {\n const other = columnNamed(columns, p.other);\n return !other || family(column) !== family(other)\n ? 'unknown'\n : compare(normalize(column, rowValue(row, p.field)), p.operator, normalize(other, rowValue(row, p.other)));\n }\n const results = p.values.map((value) => compare(normalize(column, rowValue(row, p.field)), p.operator, normalize(column, value)));\n return results.includes('unknown') ? 'unknown' : results.some((r) => r === true);\n}\n\nexport function conditionResult(\n condition: CompiledCondition,\n row: Readonly<Record<string, unknown>>,\n columns: readonly ColumnMeta[],\n): boolean | 'unknown' {\n const results = condition.map((group) => group.map((p) => comparisonResult(p, row, columns)));\n // Unknown dependencies never disappear behind a short-circuiting branch.\n if (results.flat().includes('unknown')) return 'unknown';\n return results.some((group) => group.every((r) => r === true));\n}\n\n/** Whether a record's values meet a condition: 'unknown' where a value cannot be compared, or the condition cannot be read against these columns. */\nexport function evaluateFormCondition(\n condition: FormCondition,\n row: Readonly<Record<string, unknown>>,\n columns: readonly ColumnMeta[],\n paths = false,\n): boolean | 'unknown' {\n let compiled: CompiledCondition;\n try {\n compiled = compileCondition(condition, 'Condition', columns, 'record', paths);\n } catch {\n return 'unknown';\n }\n return conditionResult(compiled, row, columns);\n}\n\n/** The word a form default fills in for its column — the constants `today`, `now`, `me` — or undefined for a literal. */\nexport function formDefaultWord(column: ColumnMeta, value: unknown): 'today' | 'now' | 'me' | undefined {\n // columnKind, not family: a SQLite TEXT declared a date (`TEXT as date`) is a date here.\n if (value === 'today' || value === 'now') return columnKind(column.type) === 'date' ? value : undefined;\n return value === 'me' && (column.references || columnKind(column.type) === 'text') ? 'me' : undefined;\n}\n\n/**\n * A New form's starting values the schema already says: a vocabulary column's\n * literal DEFAULT (`status … default 'booked'`) is the choice the form opens\n * on, so the person sees what the database would write. A clock or a sequence\n * stays the database's to fill. The form's own `defaults` win.\n */\nexport function inferredFormDefaults(table: { columns: readonly ColumnMeta[] }): Record<string, string> {\n const out: Record<string, string> = {};\n for (const column of table.columns) {\n if (!column.allowed?.length || column.flag || column.default === undefined) continue;\n const literal = /^'((?:[^']|'')*)'(?:::[\\w\\s.\"]+)?$/.exec(column.default.trim())?.[1]?.replace(/''/g, \"'\") ?? column.default.trim();\n if (column.allowed.includes(literal)) out[column.name] = literal;\n }\n return out;\n}\n\n/** A stored value as the evaluator reads it, by its column's kind: a number, a yes-or-no, a day, text; null for none. */\nexport function typedValue(table: TableMeta, column: string, value: unknown): ComputedValue {\n if (value === null || value === undefined || value === '') return null;\n const meta = table.columns.find((c) => c.name === column);\n if (!meta) return null;\n if (isFlagColumn(meta)) return value === true || value === 1 || /^(1|true|t|yes|y)$/i.test(String(value));\n const kind = columnKind(meta.type);\n if (kind === 'number') {\n const n = Number(value);\n return Number.isFinite(n) ? n : null;\n }\n if (kind === 'date') {\n const text = String(value);\n return /^\\d{4}-\\d{2}-\\d{2}$/.test(text) ? { day: text } : Number.isNaN(Date.parse(text)) ? null : { instant: new Date(text) };\n }\n return String(value);\n}\n","/**\n * The icons an App may name, and the check that an authored name is one.\n *\n * A menu entry's icon is a word in the manifest (`icon: 'kanban'`), never a\n * path or a component: a manifest is data, served to the browser and checked\n * by the server, and a word is what both sides can agree on. The words are\n * Phosphor's own names (https://phosphoricons.com, MIT, Phosphor Icons), so\n * an author can look one up there and see exactly what they will get; the\n * glyphs are drawn by `@phosphor-icons/react`, which the client already\n * depends on (packages/kit/src/client/components/Icon.tsx imports each one by itself).\n *\n * The set is curated rather than the whole pack of ~1,500: the icons business\n * Apps put in a menu — places, people, things, money, documents, time, state,\n * and a clinic's, a school's, a workshop's — and the marks a value or a change\n * wears (a display rule's `icons`, an activity feed's resources), and not the\n * glyphs neither has a use for. A glyph costs an App nothing it does not use:\n * the server sends the drawings of the icons an App names with its payload\n * (packages/mistri/src/server/app/iconPayload.ts), and the client bundles none of the set. So\n * the set grows by a word here and `node scripts/generate-icons.mjs`; the\n * tests keep this list, the JSON schema's `appIcon` enum, packages/mistri/schema/app.d.ts's\n * `AppIconName` and the generated glyphs the same.\n */\n\nexport const APP_ICONS = [\n // Places and structure\n 'house',\n 'tray',\n 'list',\n 'list-checks',\n 'kanban',\n 'table',\n 'calendar',\n 'calendar-check',\n 'clock',\n 'chart-bar',\n 'chart-line',\n 'chart-pie',\n 'gauge',\n 'target',\n 'flag',\n // People and organizations\n 'user',\n 'users',\n 'user-circle',\n 'address-book',\n 'handshake',\n 'buildings',\n 'storefront',\n 'briefcase',\n // Things and logistics\n 'package',\n 'truck',\n 'car',\n 'shopping-cart',\n // Money\n 'receipt',\n 'currency-dollar',\n 'credit-card',\n 'wallet',\n 'bank',\n // Documents\n 'file',\n 'file-text',\n 'folder',\n 'clipboard-text',\n 'note',\n 'archive',\n 'tag',\n 'ticket',\n 'star',\n 'bookmark-simple',\n // Communication\n 'bell',\n 'envelope',\n 'chat-circle',\n 'phone',\n 'headset',\n 'megaphone',\n 'globe',\n 'map-pin',\n // Tools and state\n 'magnifying-glass',\n 'gear',\n 'sliders',\n 'wrench',\n 'bug',\n 'shield-check',\n 'lock',\n 'key',\n 'database',\n 'stack',\n 'lightning',\n 'check-circle',\n 'warning',\n 'info',\n // Values: a priority's signal bars, a status's mark, what a change did\n 'cell-signal-low',\n 'cell-signal-medium',\n 'cell-signal-high',\n 'cell-signal-full',\n 'fire',\n 'hourglass',\n 'circle-dashed',\n 'x-circle',\n 'plus-circle',\n 'pencil-simple',\n 'paper-plane-tilt',\n 'arrows-left-right',\n 'arrow-circle-down',\n 'arrow-circle-up',\n // Devices: what a service catalog orders and an asset list holds\n 'laptop',\n 'monitor',\n 'device-mobile',\n 'printer',\n // Health and care: a clinic, a pharmacy, a lab\n 'stethoscope',\n 'heartbeat',\n 'heart',\n 'pill',\n 'syringe',\n 'first-aid-kit',\n 'thermometer',\n 'flask',\n 'test-tube',\n 'microscope',\n 'bed',\n 'hospital',\n 'ambulance',\n 'tooth',\n 'eyeglasses',\n 'wheelchair',\n 'baby',\n 'dna',\n 'brain',\n 'bandaids',\n 'hand-heart',\n // Learning, land and buildings\n 'graduation-cap',\n 'book-open',\n 'student',\n 'chalkboard-teacher',\n 'factory',\n 'warehouse',\n 'house-line',\n 'plant',\n 'leaf',\n 'sun',\n 'cloud',\n 'drop',\n // Travel\n 'airplane',\n 'bicycle',\n 'bus',\n 'train',\n 'boat',\n // Things, tools and records\n 'hammer',\n 'toolbox',\n 'plug',\n 'lightbulb',\n 'cube',\n 'camera',\n 'image',\n 'trophy',\n 'gift',\n 'coffee',\n 'fork-knife',\n 'shopping-bag',\n 'basket',\n 'barcode',\n 'qr-code',\n 'scales',\n 'gavel',\n 'certificate',\n 'identification-card',\n 'fingerprint',\n 'eye',\n 'link',\n 'paperclip',\n 'scissors',\n 'palette',\n 'code',\n 'terminal',\n 'users-three',\n 'user-plus',\n] as const;\n\nexport type AppIconName = (typeof APP_ICONS)[number];\n\nconst KNOWN: ReadonlySet<string> = new Set(APP_ICONS);\n\nexport const isAppIcon = (value: unknown): value is AppIconName => typeof value === 'string' && KNOWN.has(value);\n\n/* Edit distance, for \"did you mean\": a typo is a letter or two away, and a\n near synonym usually shares its first word (\"chart\" → chart-bar). */\nfunction distance(a: string, b: string): number {\n const row = Array.from({ length: b.length + 1 }, (_, i) => i);\n for (let i = 1; i <= a.length; i++) {\n let previous = row[0];\n row[0] = i;\n for (let j = 1; j <= b.length; j++) {\n const kept = row[j];\n row[j] = Math.min(row[j] + 1, row[j - 1] + 1, previous + (a[i - 1] === b[j - 1] ? 0 : 1));\n previous = kept;\n }\n }\n return row[b.length];\n}\n\nfunction nearest(name: string): string[] {\n const lower = name.toLowerCase();\n const stem = lower.split('-')[0];\n return APP_ICONS.map((icon) => ({ icon, d: icon.startsWith(stem) || icon.split('-').includes(stem) ? 0 : distance(lower, icon) }))\n .filter((each) => each.d <= 3)\n .sort((a, b) => a.d - b.d || a.icon.localeCompare(b.icon))\n .slice(0, 3)\n .map((each) => each.icon);\n}\n\n/** An authored `icon`: one of APP_ICONS, or refused with the closest names and where the rest are listed.\n * `key` is the key the author wrote it under — `ui.logo` takes an icon too, and a refusal naming \"icon\" sent people looking for a key they never wrote. */\nexport function parseIcon(value: unknown, at: string, key = 'icon'): AppIconName {\n if (isAppIcon(value)) return value;\n if (typeof value !== 'string' || !value.trim())\n throw new Error(`${at} has ${/^[aeiou]/.test(key) ? 'an' : 'a'} \"${key}\" that is not an icon's name.`);\n const near = nearest(value.trim());\n throw new Error(\n `${at} has \"${key}\": \"${value}\", which is not one of the App icons.` +\n `${near.length ? ` Did you mean ${near.map((each) => `\"${each}\"`).join(', ')}?` : ''} The icons: ${APP_ICONS.join(', ')}.`,\n );\n}\n","import { authoredValue } from './autoValues.js';\nimport { autoKey, isRequired } from './form.js';\nimport { findTable, primaryKey, type DatabaseSchema, type TableMeta } from './schema.js';\nimport { hopName } from './view.js';\nimport { parseQuery } from './query.js';\nimport type { RelatedCreate } from '../../schema/model.js';\n\ntype Scalar = string | number | boolean | null;\nconst CREATE_KEYS = ['label', 'show', 'defaults', 'prefill'];\n/** Columns filled from the page's record, at most. */\nconst MAX_FILL = 8;\nconst COLUMN = /^[A-Za-z_][A-Za-z0-9_]*$/;\n\n// A tenant host hands its pages over as captured, null-prototype copies (evidence.ts), and those are plain too.\nconst isPlain = (value: unknown): value is Record<string, unknown> =>\n !!value && typeof value === 'object' && !Array.isArray(value) && [null, Object.prototype].includes(Object.getPrototypeOf(value));\n\nexport function parseRelatedCreate(raw: unknown, at: string): RelatedCreate<string> {\n if (typeof raw === 'string') raw = { label: raw };\n if (!isPlain(raw)) throw new Error(`${at}: create must be an object.`);\n const value = raw;\n if (Object.keys(value).some((key) => !CREATE_KEYS.includes(key)))\n throw new Error(`${at}: create accepts label, show, defaults and prefill.`);\n if (typeof value.label !== 'string' || !value.label.trim()) throw new Error(`${at}: create needs a label.`);\n if (value.show !== undefined && (!Array.isArray(value.show) || value.show.some((c) => typeof c !== 'string' || !c.trim())))\n throw new Error(`${at}: create.show must contain column names.`);\n let defaults: Record<string, Scalar> | undefined;\n if (value.defaults !== undefined) {\n if (!isPlain(value.defaults)) throw new Error(`${at}: create.defaults is an object of column → value.`);\n defaults = {};\n for (const [column, authored] of Object.entries(value.defaults)) {\n if (!COLUMN.test(column)) throw new Error(`${at}: create.defaults \"${column}\" is not a column name.`);\n const v = authoredValue(authored, `${at}: create.defaults \"${column}\"`);\n if (!(v === null || typeof v === 'string' || typeof v === 'boolean' || (typeof v === 'number' && Number.isFinite(v)))) {\n throw new Error(`${at}: create.defaults \"${column}\" is a text, number, true/false, null, today, now or me.`);\n }\n defaults[column] = v as Scalar;\n }\n }\n let prefill: Record<string, string> | undefined;\n if (value.prefill !== undefined) {\n const pairs = isPlain(value.prefill) ? Object.entries(value.prefill) : [];\n if (\n !pairs.length ||\n pairs.length > MAX_FILL ||\n pairs.some(([child, parent]) => !COLUMN.test(child) || typeof parent !== 'string' || !COLUMN.test(parent))\n ) {\n throw new Error(\n `${at}: create.prefill maps one to ${MAX_FILL} of the new row's columns to the record's: { patient_id: 'patient_id' }.`,\n );\n }\n prefill = Object.fromEntries(pairs) as Record<string, string>;\n }\n return {\n label: value.label.trim(),\n ...(value.show === undefined ? {} : { show: (value.show as string[]).map((c) => c.trim()) }),\n ...(defaults ? { defaults } : {}),\n ...(prefill ? { prefill } : {}),\n };\n}\n\n/** Whether a create says more than its label, so it cannot be written as the label alone. */\nexport function createSaysMore(create: RelatedCreate<string>): boolean {\n return create.show !== undefined || create.defaults !== undefined || create.prefill !== undefined;\n}\n\n/**\n * What a filtered related list's query pins: its equalities (`status = open`,\n * `kind in (visit)`), read by the query language's own parser over the child\n * table. A row added from the list starts with them, so it is listed where it\n * was added. An `or`, a range or a path pins nothing.\n */\nfunction filterEqualities(schema: DatabaseSchema, from: TableMeta, filter: string | undefined): Record<string, Scalar> {\n if (!filter?.trim()) return {};\n const parsed = parseQuery(`${from.id} ${filter}`, { schema });\n const groups = parsed.query?.filter.groups;\n if (parsed.errors.length || !groups || groups.length !== 1) return {};\n const pinned: Record<string, Scalar> = {};\n for (const condition of groups[0]) {\n if (condition.column.includes('.') || !from.columns.some((c) => c.name === condition.column)) continue;\n const value =\n condition.op === '='\n ? condition.value\n : condition.op === 'in' && Array.isArray(condition.value) && condition.value.length === 1\n ? condition.value[0]\n : undefined;\n if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') pinned[condition.column] = value;\n }\n return pinned;\n}\n\n/** Bind only a direct FK to the whole parent PK, never inferred query equalities.\n * `filter` is the related list's own query; the values it pins fill the new row\n * as defaults do, never as bindings. */\nexport function relatedCreatePlan(\n schema: DatabaseSchema,\n baseId: string,\n fromId: string,\n path: string,\n create: RelatedCreate<string>,\n list: { filter?: string } = {},\n) {\n const base = findTable(schema, baseId);\n const from = findTable(schema, fromId);\n const candidates = schema.foreignKeys.filter((fk) => fk.from.table === fromId);\n const fk = candidates.find((fk) => fk.name === path) ?? candidates.find((fk) => hopName(fk) === path);\n /* A wrong `whose` is the usual mistake, so it is named for what it is —\n not a column, or a column that points elsewhere — with the references\n that would do, rather than as a rule about primary keys. */\n if (base && from && !fk && !path.includes('.')) {\n const toBase = candidates.filter((ref) => ref.to.table === baseId).map((ref) => `\"${hopName(ref)}\"`);\n const could = toBase.length\n ? ` Its references to ${base.name}: ${toBase.join(', ')}.`\n : ` ${from.name} has no reference to ${base.name}.`;\n return {\n error: from.columns.some((c) => c.name === path)\n ? `\"${path}\" is a column of ${from.name}, but not a reference to ${base.name}.${could}`\n : `\"${path}\" is not a column of ${from.name}.${could}`,\n };\n }\n if (base && fk && fk.to.table !== baseId) {\n return {\n error: `\"${path}\" refers to ${findTable(schema, fk.to.table)?.name ?? fk.to.table}, not to ${base.name}, so a row added here would not belong to this record.`,\n };\n }\n if (\n !base ||\n !from ||\n from.isView ||\n !fk ||\n fk.to.columns.length !== primaryKey(base).length ||\n !primaryKey(base).every((c) => fk.to.columns.includes(c))\n ) {\n return { error: 'Related creation needs a direct reference to the complete parent primary key.' };\n }\n const show = create.show ?? from.columns.map((c) => c.name);\n if (show.some((name) => !from.columns.some((c) => c.name === name))) return { error: 'Related creation names an unknown form column.' };\n const unknownDefault = Object.keys(create.defaults ?? {}).find((name) => !from.columns.some((c) => c.name === name));\n if (unknownDefault) return { error: `Related creation defaults \"${unknownDefault}\", which is not a column of ${from.name}.` };\n /* A filled column comes from the record: the child's column, the record's column. */\n const fill: Record<string, string> = { ...(create.prefill ?? {}) };\n for (const [child, parent] of Object.entries(fill)) {\n if (!from.columns.some((c) => c.name === child))\n return { error: `Related creation fills \"${child}\", which is not a column of ${from.name}.` };\n if (!base.columns.some((c) => c.name === parent))\n return { error: `Related creation fills \"${child}\" from \"${parent}\", which is not a column of ${base.name}.` };\n if (fk.from.columns.includes(child))\n return { error: `Related creation fills \"${child}\", the reference to ${base.name} it already binds.` };\n }\n const pinned = filterEqualities(schema, from, list.filter);\n const given = new Set([...Object.keys(create.defaults ?? {}), ...Object.keys(fill), ...Object.keys(pinned)]);\n const assigned = autoKey(from);\n const hiddenRequired = from.columns.filter(\n (c) => isRequired(c, assigned) && !show.includes(c.name) && !fk.from.columns.includes(c.name) && !given.has(c.name),\n );\n if (hiddenRequired.length)\n return { error: `Related creation must show required columns: ${hiddenRequired.map((c) => c.name).join(', ')}.` };\n /* A required reference to another record (a work order's bike, added from its mechanic): the record cannot say it,\n so a New the list offers on its own would ask a whole other choice; one the author writes still may. */\n const otherParents = schema.foreignKeys\n .filter((ref) => ref !== fk && ref.from.table === fromId)\n .flatMap((ref) => ref.from.columns)\n .filter((name, index, all) => all.indexOf(name) === index && !fk.from.columns.includes(name) && !given.has(name))\n .filter((name) => {\n const column = from.columns.find((c) => c.name === name);\n return column !== undefined && isRequired(column, assigned);\n });\n return {\n show: show.filter((c) => !fk.from.columns.includes(c)),\n bindings: fk.from.columns.map((column, i) => ({ column, parent: fk.to.columns[i] })),\n otherParents,\n // The list's own equalities win: a row added here is listed here.\n defaults: { ...(create.defaults ?? {}), ...pinned },\n fill,\n };\n}\n","/**\n * An App's addresses: one canonical URI per thing it shows.\n *\n * A record is `/{app}/{route}/{id}` — `/compass/leads/127` — where the route\n * is the view that shows its table, so following a reference from a lead to\n * its account opens `/compass/accounts/13`, the account's own address, not a\n * path that grows with every hop. The walk that got there rides in history\n * state (client url.js), so Back and the crumbs still know it, and a link\n * pasted into chat is the record alone.\n *\n * The route a table's records are addressed through is decided once, here,\n * the same way for the client, the server and `mistri check`, over the\n * views the person reading is served — so a resident's repair is\n * `/my-requests/12` and the office's the same record at `/repairs/12`:\n *\n * 1. the resource's `view`, when it names one of the list views over it\n * that this reader is served — a preference, passed over quietly for a\n * person whose role is not served that view;\n * 2. else the first list view over the table in the menu's order that has\n * no standing query and is not a split — the plain list of them — then\n * the first without a standing query, then the first;\n * 3. else the page its records open through, by its `path` or key;\n * 4. else the table's own name.\n *\n * A view's route is its `path`, or its key; a page's likewise. Dashboards,\n * free-form pages and forms are their route alone. What the room on screen is showing\n * rides the address's parameters (client publicAddress.ts writes and reads\n * them; `mistri check --routes` lists each one a route reads):\n *\n * list `tab=` (a tab's label as a word; none is the first), `search=`\n * (the name column's box), one per filter column\n * (`make=Toyota`, `make=Toyota,Honda`, `price=..10000`,\n * `year=2018..`), `sort=-price,year`, and `q=` for a question\n * those cannot say, whole\n * dashboard `period=` (one of its windows, as a word; `all-time` where it\n * opens on a `window` of its own), one per pick column\n * (`owner_id=12`), and `period.<section>=` for a section with\n * windows of its own, the section named by its title\n * free-form the parameters its `definePage` declares (`params: ['list',\n * page 'line']` → `?list=12&line=3`), and nothing else\n *\n * A parameter a route does not offer is not read: a link naming a window or\n * a column the view no longer has lands on the view as it opens.\n *\n * The id segment is the primary key's value, or — where the resource says\n * `address: '<column>'` — that column's (a natural key the check proves NOT\n * NULL and unique). Composite keys are said under their parent where one of\n * their two columns is a reference to it (`/orders/12/lines/3`), otherwise\n * by name (`order_id=12,line=3`). A slug (`slug: true`) follows a numeric id\n * on a public page — `/forecourt/vehicles/1-2021-toyota-corolla` — and only\n * the leading id is read back, so any spelling of the rest finds the record.\n *\n * Values stay text end to end: an id past 2^53 is a string from the adapter\n * and a string here, never run through Number().\n *\n * Pure and dependency-light on purpose: the server's canonical redirect,\n * the check's route table and the browser's address bar must never disagree\n * about what an address means.\n */\nimport { filterAddressWord } from '@mistriwork/engine/shared/listFilters.js';\nimport { slug as slugOf } from '@mistriwork/engine/shared/slug.js';\nimport { tabKey } from '@mistriwork/engine/shared/addressParams.js';\nimport { SERVER_SEGMENTS } from '@mistriwork/engine/shared/mount.js';\nimport { ASSISTANT_ROOM } from '@mistriwork/engine/shared/servedApp.js';\nexport { ASSISTANT_ROOM } from '@mistriwork/engine/shared/servedApp.js';\n\n/** Words an App route may not take: `new` is the New form's, the rest the server's own (mount.ts). */\nexport const RESERVED_PATHS: readonly string[] = ['new', ...SERVER_SEGMENTS, '_components', 'sign-in', 'welcome', 'form'];\n\n/** The signed-in person's notifications, in an App that keeps an inbox (appNotify.ts). */\nexport const INBOX_SEGMENT = 'notifications';\n\n/** One route segment as an author spells it in `path:`: the letters a view key allows. */\nconst ROUTE_PATH = /^[a-z0-9][a-z0-9-]{0,63}$/;\n\ninterface ColumnLike {\n name: string;\n primaryKey: boolean;\n references?: { table: string; column: string };\n}\ninterface TableLike {\n id: string;\n name: string;\n columns: readonly ColumnLike[];\n}\ninterface ViewLike {\n table?: string;\n path?: string;\n query?: string;\n presentation?: { kind?: string };\n sections?: unknown;\n command?: unknown;\n}\n\nexport interface AddressBookInput {\n /** The App's views by key: a list names its `table`, a dashboard has `sections`, a form a `command`. */\n views: Readonly<Record<string, ViewLike>>;\n /** View keys in the menu's order (viewOrder): which list is a table's first. */\n order: readonly string[];\n /** What each resource says about its addresses, by resolved table id. */\n resources: Readonly<Record<string, { view?: string; address?: string; slug?: boolean }>>;\n /** The App's pages in load order — the first over a base is the one its records open through. */\n pages: readonly { id: string; base: string; path?: string; document?: unknown }[];\n tables: readonly TableLike[];\n /** The App keeps an inbox, whose room is `/{app}/notifications`. */\n inbox?: boolean;\n /** The assistant has a page of its own, `/{app}/assistant`, for the person served (appPayload.ts). */\n assistant?: boolean;\n /** The views are what one person is served, not the whole App's: a resource's `view` naming one they are not\n * served is that person's to pass over, not a problem (the whole App's book still says a misnamed one). */\n projected?: boolean;\n}\n\n/** What a route's segment names. */\nexport type RouteTarget =\n | {\n kind: 'list';\n view: string;\n table: string;\n /** The words of the tabs after the first, which is the list as it opens (`?tab=decided`). */\n tabs?: string[];\n /** The filter columns it is refined by (`?make=Toyota`). */\n filters?: string[];\n }\n | {\n kind: 'dashboard';\n view: string;\n /** The dashboard offers windows of its own (`?period=`). */\n window?: true;\n /** The columns its picks are said by (`?owner_id=12`). */\n picks?: string[];\n /** Sections with windows of their own, by the name each rides the address under (`?period.<name>=`). */\n periods?: string[];\n /** A free-form page (`definePage`), drawn as a dashboard of its one component. */\n freeForm?: true;\n /** A free-form page's parameters it keeps its place in (`?list=12&line=3`). */\n params?: string[];\n }\n | { kind: 'form'; view: string }\n | { kind: 'page'; page: string; table: string }\n | { kind: 'table'; table: string }\n | { kind: 'inbox' }\n | { kind: 'assistant' };\n\n/** How one table's records are addressed. */\ninterface RecordRoute {\n segment: string;\n table: string;\n target: RouteTarget;\n /** The natural key column (`address:`), when the resource names one. */\n column?: string;\n /** A cosmetic slug follows a numeric id on public pages. */\n slug?: true;\n /** A two-column key one of whose columns references its parent: said under the parent's address. */\n parent?: { column: string; table: string; child: string };\n}\n\nexport interface AddressBook {\n /** Segment → what it names. */\n routes: Map<string, RouteTarget>;\n /** View key → its segment. */\n views: Map<string, string>;\n /** Page id → its segment, where it has one of its own. */\n pages: Map<string, string>;\n /** Page id → its base table. */\n pageBase: Map<string, string>;\n /** Table id → the page its records open through. */\n defaultPage: Map<string, string>;\n /** Table id → how its records are addressed. */\n records: Map<string, RecordRoute>;\n tables: Map<string, TableLike>;\n /** Collisions and misnamed routes, for the check and the shell's count. */\n problems: string[];\n}\n\n/** An authored `path:` — one route segment, not a reserved word — or a refusal that names the fix. */\nexport function parseRoutePath(value: unknown, at: string): string | undefined {\n if (value === undefined) return undefined;\n if (typeof value !== 'string' || !ROUTE_PATH.test(value)) {\n throw new Error(`${at}: \"path\" must be one address segment — lowercase letters, digits and hyphens, like \"sales-dashboard\".`);\n }\n if (RESERVED_PATHS.includes(value)) throw new Error(`${at}: \"path\" cannot be \"${value}\"; ${RESERVED_PATHS.join(', ')} are reserved.`);\n return value;\n}\n\nconst isDashboard = (view: ViewLike) => Object.hasOwn(view, 'sections');\nconst words = (value: unknown): string[] => (Array.isArray(value) ? value.filter((word): word is string => typeof word === 'string') : []);\n/* What a dashboard's address may say beyond its route: its window, its picks,\n each windowed section's own window — or, for a free-form page, the place it keeps. */\nfunction parametersOf(\n view: ViewLike & { aside?: unknown; windows?: unknown; filters?: unknown; page?: { params?: unknown } },\n): Omit<Extract<RouteTarget, { kind: 'dashboard' }>, 'kind' | 'view'> {\n if (view.page) {\n const params = words(view.page.params);\n return { freeForm: true as const, ...(params.length ? { params } : {}) };\n }\n const periods = [view.sections, view.aside]\n .flatMap((list) => (Array.isArray(list) ? list : []))\n .flatMap((section: { windowKey?: unknown }) => (typeof section?.windowKey === 'string' ? [section.windowKey] : []));\n const picks = words(view.filters);\n return {\n ...(words(view.windows).length ? { window: true as const } : {}),\n ...(picks.length ? { picks } : {}),\n ...(periods.length ? { periods } : {}),\n };\n}\n/** A list's parameters as the route table says them: its tabs by their words, and its filter columns. */\nfunction listParametersOf(\n view: ViewLike & { tabs?: unknown; filters?: unknown },\n): Omit<Extract<RouteTarget, { kind: 'list' }>, 'kind' | 'view' | 'table'> {\n const tabs = (Array.isArray(view.tabs) ? view.tabs : [])\n .slice(1)\n .flatMap((tab: { label?: unknown; slug?: string }) =>\n typeof tab?.label === 'string' && tabKey({ label: tab.label, slug: tab.slug }) ? [tabKey({ label: tab.label, slug: tab.slug })] : [],\n );\n // Each by its address word: a `has` filter's is `has.<table>.<column>` (listFilters.ts).\n const filters = words(view.filters).map(filterAddressWord);\n return { ...(tabs.length ? { tabs } : {}), ...(filters.length ? { filters } : {}) };\n}\nconst isForm = (view: ViewLike) => Object.hasOwn(view, 'command');\nconst isList = (view: ViewLike | undefined): view is ViewLike & { table: string } =>\n Boolean(view && !isDashboard(view) && !isForm(view) && typeof view.table === 'string');\n\nconst describe = (target: RouteTarget): string => {\n switch (target.kind) {\n case 'list':\n return `the list view \"${target.view}\"`;\n case 'dashboard':\n return target.freeForm ? `the free-form page \"${target.view}\"` : `the dashboard \"${target.view}\"`;\n case 'form':\n return `the form \"${target.view}\"`;\n case 'page':\n return `the page \"${target.page}\"`;\n case 'inbox':\n return 'the notifications inbox';\n case 'assistant':\n return \"the assistant's page\";\n default:\n return `the table \"${target.table}\"`;\n }\n};\n\nconst primaryKeyOf = (table: TableLike | undefined): string[] =>\n table ? table.columns.filter((c) => c.primaryKey).map((c) => c.name) : [];\n\n/** Every route the App answers to, and how each table's records are addressed. */\nexport function appAddressBook(input: AddressBookInput): AddressBook {\n const book: AddressBook = {\n routes: new Map(),\n views: new Map(),\n pages: new Map(),\n pageBase: new Map(),\n defaultPage: new Map(),\n records: new Map(),\n tables: new Map(input.tables.map((table) => [table.id, table])),\n problems: [],\n };\n const reserved = new Set(RESERVED_PATHS);\n /* Claimed in a fixed order — views in the menu's, then pages, then the\n tables nothing else shows — so which of two claimants a collision keeps\n never depends on object key order. */\n const claim = (segment: string, target: RouteTarget, what: string, said: boolean): boolean => {\n if (reserved.has(segment)) {\n if (said) book.problems.push(`${what}: its address \"${segment}\" is reserved (${RESERVED_PATHS.join(', ')}). Give it another \"path\".`);\n return false;\n }\n\n const held = book.routes.get(segment);\n if (held) {\n if (said)\n book.problems.push(\n `${what}: its address \"/${segment}\" is already ${describe(held)}'s. Two routes cannot share a path; give one of them another \"path\".`,\n );\n return false;\n }\n book.routes.set(segment, target);\n return true;\n };\n /* Claimed first: an App that keeps an inbox has it at one address in\n every App, and a view that would take the word is told to take another. */\n if (input.inbox) book.routes.set(INBOX_SEGMENT, { kind: 'inbox' });\n if (input.assistant) book.routes.set(ASSISTANT_ROOM, { kind: 'assistant' });\n\n for (const key of input.order) {\n const view = input.views[key];\n if (!view) continue;\n const target: RouteTarget | undefined = isDashboard(view)\n ? { kind: 'dashboard', view: key, ...parametersOf(view) }\n : isForm(view)\n ? { kind: 'form', view: key }\n : isList(view)\n ? { kind: 'list', view: key, table: view.table, ...listParametersOf(view) }\n : undefined;\n if (!target) continue;\n const segment = view.path ?? key;\n // A key is always said: a view is its address, and a collision there is the author's to resolve.\n if (claim(segment, target, `views \"${key}\"`, true)) book.views.set(key, segment);\n }\n const listed = new Set([...book.views.keys()].flatMap((key) => (isList(input.views[key]) ? [input.views[key].table] : [])));\n for (const page of input.pages) {\n /* A document is said under its record (`/orders/12/invoice`, appDocuments.ts): no route, and never the page a\n record opens through. */\n if (page.document !== undefined) continue;\n book.pageBase.set(page.id, page.base);\n const first = !book.defaultPage.has(page.base);\n if (first) book.defaultPage.set(page.base, page.id);\n const segment = page.path ?? page.id;\n /* A page answers to its own address only where it needs one: its `path`\n is written, its base has no list to be addressed through, or it is a\n second page over the base. Otherwise `/compass/lead` stays what it\n always was — the lead table, read the older way — and the lead page's\n records are `/compass/leads/127`. A key a view already answers to is\n no collision the author made; a `path` they wrote is theirs, and said. */\n if (page.path === undefined && (!ROUTE_PATH.test(segment) || (first && listed.has(page.base)))) continue;\n if (claim(segment, { kind: 'page', page: page.id, table: page.base }, `pages \"${page.id}\"`, page.path !== undefined))\n book.pages.set(page.id, segment);\n }\n\n for (const table of input.tables) {\n const resource = Object.hasOwn(input.resources, table.id) ? input.resources[table.id] : undefined;\n const lists = input.order.filter((key) => book.views.has(key) && isList(input.views[key]) && input.views[key].table === table.id);\n let view: string | undefined;\n if (resource?.view !== undefined) {\n if (lists.includes(resource.view)) view = resource.view;\n else if (input.projected && !Object.hasOwn(input.views, resource.view)) view = undefined;\n else\n book.problems.push(\n `resources \"${table.name}\": \"view\" names \"${resource.view}\", which is not a list view over \"${table.name}\". It names the view its records are addressed through.`,\n );\n }\n view ??=\n lists.find((key) => !input.views[key].query && input.views[key].presentation?.kind !== 'split') ??\n lists.find((key) => !input.views[key].query) ??\n lists[0];\n let route: RecordRoute | undefined;\n if (view) route = { segment: book.views.get(view)!, table: table.id, target: { kind: 'list', view, table: table.id } };\n const page = book.defaultPage.get(table.id);\n if (!route && page && book.pages.has(page))\n route = { segment: book.pages.get(page)!, table: table.id, target: { kind: 'page', page, table: table.id } };\n if (!route) {\n const target: RouteTarget = { kind: 'table', table: table.id };\n // Only a resource the App declares is the author's to be told about.\n if (claim(table.id, target, `resources \"${table.name}\"`, Boolean(resource))) route = { segment: table.id, table: table.id, target };\n }\n if (!route) continue;\n if (resource?.address !== undefined) {\n if (table.columns.some((column) => column.name === resource.address)) route.column = resource.address;\n else\n book.problems.push(`resources \"${table.name}\": \"address\" names \"${resource.address}\", which is not a column of \"${table.name}\".`);\n }\n if (resource?.slug) route.slug = true;\n book.records.set(table.id, route);\n }\n\n /* Second pass: a parent must itself have an address said by its primary\n key, or the child's could not be written before the parent's name is\n known. */\n for (const route of book.records.values()) {\n if (route.column) continue;\n const key = book.tables.get(route.table)!.columns.filter((column) => column.primaryKey);\n if (key.length !== 2) continue;\n for (const [i, column] of key.entries()) {\n const reference = column.references;\n if (!reference || reference.table === route.table) continue;\n const parent = book.records.get(reference.table);\n const parentKey = primaryKeyOf(book.tables.get(reference.table));\n if (!parent || parent.column || parentKey.length !== 1 || parentKey[0] !== reference.column) continue;\n route.parent = { column: column.name, table: reference.table, child: key[1 - i].name };\n break;\n }\n }\n return book;\n}\n\n/* ---------- ids ---------- */\n\n/* The walk's own form spells a page `@page~id`, a list `view:query` and a\n New form `!new`: an App address escapes those marks inside a value, so a\n raw one in a segment always means the older form (readAppSegments). */\nconst encode = (value: unknown) =>\n encodeURIComponent(String(value)).replace(/[!'()*~]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);\nconst OLDER_MARKS = /[@~!:]/;\n\n/** A key named column by column: `order_id=12,line=3`, each value escaped so a `,` or `=` in one is data. */\nfunction namedKey(key: Readonly<Record<string, unknown>>, columns: readonly string[]): string | undefined {\n if (!columns.length || columns.some((column) => key[column] === undefined || key[column] === null)) return undefined;\n return columns.map((column) => `${column}=${encode(key[column])}`).join(',');\n}\n\n/** The text a record's name becomes after its id: \"2021 Toyota Corolla\" → \"2021-toyota-corolla\". */\nexport function addressSlug(label: string | undefined): string {\n return label ? slugOf(label, '') : '';\n}\n\ninterface RecordAddress {\n /** The record's key: its primary key, as the walk holds it. */\n key: Readonly<Record<string, unknown>>;\n /** The natural key's value, when the route has one and it is known. */\n natural?: unknown;\n /** The record's name, for a slug where the route takes one. */\n slug?: string;\n /** The tab open on it, as its word. */\n tab?: string;\n /** The segment to say it under, when not the table's own: a split's, a second page's. */\n via?: string;\n}\n\n/**\n * A record's address after the App's id: `['leads', '127']`. Undefined when\n * the table has no route, or the key is incomplete — the caller writes the\n * walk's own form then. A natural-key route whose value is not known yet is\n * said by its primary key, named (`id=12`), which reads back without doubt.\n */\nexport function recordSegments(book: AddressBook, table: string, at: RecordAddress): string[] | undefined {\n const route = book.records.get(table);\n if (!route) return undefined;\n const pk = primaryKeyOf(book.tables.get(table));\n if (!pk.length) return undefined;\n const tab = at.tab ? [encode(at.tab)] : [];\n const segment = encode(at.via ?? route.segment);\n if (route.column) {\n if (\n at.natural !== undefined &&\n at.natural !== null &&\n String(at.natural) !== '' &&\n !String(at.natural).includes('=') &&\n String(at.natural) !== 'new'\n ) {\n return [segment, encode(at.natural), ...tab];\n }\n const named = namedKey(at.key, pk);\n return named ? [segment, named, ...tab] : undefined;\n }\n if (route.parent && at.via === undefined) {\n const parent = book.records.get(route.parent.table)!;\n const up = at.key[route.parent.column],\n down = at.key[route.parent.child];\n if (up !== undefined && up !== null && down !== undefined && down !== null) {\n return [encode(parent.segment), single(up), segment, single(down), ...tab];\n }\n }\n if (pk.length === 1) {\n const value = at.key[pk[0]];\n if (value === undefined || value === null) return undefined;\n let id = single(value, pk[0]);\n const words = route.slug && at.slug && /^\\d+$/.test(String(value)) ? at.slug : '';\n if (words) id += `-${words}`;\n return [segment, id, ...tab];\n }\n const named = namedKey(at.key, pk);\n return named ? [segment, named, ...tab] : undefined;\n}\n\n/** One id, bare — unless bare would be read as something else: `new`, or nothing. */\nfunction single(value: unknown, column?: string): string {\n const text = String(value);\n if (column && (text === 'new' || text === '')) return `${column}=${encode(text)}`;\n return encode(text);\n}\n\n/* ---------- reading ---------- */\n\ntype ParsedAddress =\n | { kind: 'route'; target: RouteTarget }\n | { kind: 'new'; target: RouteTarget & { table: string } }\n /** One of the person's conversations with the assistant: `/assistant/{id}`. */\n | { kind: 'conversation'; target: RouteTarget; id: string }\n | {\n kind: 'record';\n target: RouteTarget;\n table: string;\n key: Record<string, string>;\n natural?: { column: string; value: string };\n tab?: string;\n slugged?: boolean;\n };\n\nconst decode = (text: string): string | undefined => {\n try {\n return decodeURIComponent(text);\n } catch {\n return undefined;\n }\n};\n\n/**\n * An address after the App's id, read: the segments as the browser sent\n * them, still escaped. Undefined for anything that is not an App address —\n * a query-language segment, the walk's `@`/`~` forms — which the caller\n * reads the older way and rewrites.\n */\nexport function readAppSegments(book: AddressBook, raw: readonly string[]): ParsedAddress | undefined {\n if (!raw.length || raw.some((segment) => OLDER_MARKS.test(segment))) return undefined;\n const head = decode(raw[0]);\n /* A table's own name over a record (`/desk/incident/BLU-201`, where the\n incident's view is `tickets`) is read under the route its records are\n addressed through, and so said there — never as the older reading's\n table and question, which made a list of the id. The name alone stays\n the older reading's: `/compass/lead` is the lead table. */\n const named =\n head === undefined || raw.length < 2\n ? undefined\n : book.tables.has(head)\n ? book.records.get(head)\n : /* A page's id over a record is the same: a page that had no list view was `/deal/1270`, and a list view\n over its table, added later, moved the records to the list's address; the old one still opens them. */\n book.records.get(book.pageBase.get(head) ?? '');\n const target = head === undefined ? undefined : (book.routes.get(head) ?? (named && book.routes.get(named.segment)));\n if (!target) return undefined;\n if (raw.length === 1) return { kind: 'route', target };\n if (target.kind === 'assistant') {\n const id = raw.length === 2 ? decode(raw[1]) : undefined;\n return id ? { kind: 'conversation', target, id } : undefined;\n }\n if (target.kind === 'dashboard' || target.kind === 'form' || target.kind === 'inbox') return undefined;\n if (raw.length === 2 && raw[1] === 'new') return target.kind === 'page' ? undefined : { kind: 'new', target };\n const table = target.table;\n const read = readKey(book, table, raw[1]);\n if (!read) return undefined;\n if (raw.length === 2 || raw.length === 3) {\n const tab = raw.length === 3 ? decode(raw[2]) : undefined;\n if (raw.length === 3 && !tab) return undefined;\n return { kind: 'record', target, table, ...read, ...(tab ? { tab } : {}) };\n }\n if (raw.length > 5 || read.natural) return undefined;\n // A child keyed under this record: `/orders/12/lines/3`.\n const under = decode(raw[2]);\n const childTarget = under === undefined ? undefined : book.routes.get(under);\n const childTable = childTarget && 'table' in childTarget ? childTarget.table : undefined;\n const child = childTable ? book.records.get(childTable) : undefined;\n const pk = primaryKeyOf(book.tables.get(table));\n const down = decode(raw[3]);\n if (!childTarget || !child?.parent || child.parent.table !== table || pk.length !== 1 || down === undefined) return undefined;\n const tab = raw.length === 5 ? decode(raw[4]) : undefined;\n if (raw.length === 5 && !tab) return undefined;\n return {\n kind: 'record',\n target: childTarget,\n table: child.table,\n key: { [child.parent.column]: read.key[pk[0]], [child.parent.child]: down },\n ...(tab ? { tab } : {}),\n };\n}\n\n/** An id segment as the table's route reads it. */\nfunction readKey(\n book: AddressBook,\n table: string,\n raw: string,\n): { key: Record<string, string>; natural?: { column: string; value: string }; slugged?: boolean } | undefined {\n const route = book.records.get(table);\n const pk = primaryKeyOf(book.tables.get(table));\n if (!pk.length) return undefined;\n if (raw.includes('=')) {\n const key: Record<string, string> = {};\n for (const pair of raw.split(',')) {\n const eq = pair.indexOf('=');\n const column = decode(pair.slice(0, eq)),\n value = decode(pair.slice(eq + 1));\n if (eq <= 0 || column === undefined || value === undefined || Object.hasOwn(key, column)) return undefined;\n key[column] = value;\n }\n const names = Object.keys(key);\n if (names.length === pk.length && pk.every((column) => Object.hasOwn(key, column))) return { key };\n if (route?.column && names.length === 1 && names[0] === route.column)\n return { key, natural: { column: route.column, value: key[route.column] } };\n return undefined;\n }\n const value = decode(raw);\n if (value === undefined || value === '') return undefined;\n if (route?.column) return { key: { [route.column]: value }, natural: { column: route.column, value } };\n if (pk.length !== 1) return undefined;\n if (route?.slug) {\n const leading = /^(\\d+)(?:-.*)?$/s.exec(value);\n if (leading) return { key: { [pk[0]]: leading[1] }, ...(leading[1] !== value ? { slugged: true } : {}) };\n }\n return { key: { [pk[0]]: value } };\n}\n\n/* ---------- the check ---------- */\n\ninterface CatalogColumn extends ColumnLike {\n nullable: boolean;\n}\ninterface CatalogTable extends TableLike {\n columns: readonly CatalogColumn[];\n unique?: readonly string[];\n uniquePairs?: ReadonlyArray<readonly [string, string]>;\n}\n\n/**\n * A natural key is an address only where the database keeps it one: NOT\n * NULL and unique by a constraint or a unique index the catalog reports.\n * Two rows under one key would make a shared link open whichever the\n * database found first, and a null would have no address at all, so the\n * check refuses rather than warns. The primary key alone proves itself.\n */\nexport function naturalKeyProblems(book: AddressBook, tenantOwned: (table: string) => boolean = () => false): string[] {\n const problems: string[] = [];\n for (const route of book.records.values()) {\n if (!route.column) continue;\n const table = book.tables.get(route.table) as CatalogTable | undefined;\n const column = table?.columns.find((candidate) => candidate.name === route.column);\n if (!table || !column) continue;\n const at = `resources \"${table.name}\": \"address\" names \"${column.name}\"`;\n if (column.nullable) problems.push(`${at}, which can be null. A natural key must be NOT NULL: a record without one has no address.`);\n const pk = primaryKeyOf(table);\n /* A tenant's rows are addressed within the tenant (every read is the\n session's tenant's), so a key unique beside one other column is one:\n `UNIQUE (tenant_id, number)` gives each customer its own ticket 1.\n Which column that is, the tenant policy says, not the App; but a\n tenant-owned table's every unique index must include its tenant\n column or the deployment refuses to start (tenantUniqueRefusal), so a\n pair that holds here, there holds with the tenant column. */\n const tenant = tenantOwned(table.id);\n const proven =\n (pk.length === 1 && pk[0] === column.name) ||\n Boolean(table.unique?.includes(column.name)) ||\n (tenant && Boolean(table.uniquePairs?.some((pair) => pair.includes(column.name) && pair[0] !== pair[1])));\n if (!proven)\n problems.push(\n `${at}, which the database does not keep unique. ${\n tenant\n ? `Add a UNIQUE constraint on it within its tenant, \"UNIQUE (<tenant column>, ${column.name})\"`\n : `Add a UNIQUE constraint or a unique index on \"${column.name}\" alone`\n }, so two records can never share an address.`,\n );\n }\n return problems;\n}\n\n/* ---------- the route table ---------- */\n\n/**\n * An App mounted at the root beside other Apps (mount.ts): `/billing` is the\n * Billing App's before it is any route of this one, so a route of that name\n * could never be reached. `taken` is each other App's segment, by its id.\n */\nexport function mountClashes(book: AddressBook, taken: Readonly<Record<string, string>>): string[] {\n return Object.entries(taken).flatMap(([app, segment]) => {\n const target = book.routes.get(segment);\n return target\n ? [\n `${describe(target)}: its address \"/${segment}\" is where the App \"${app}\" is mounted, beside this one at the root. ` +\n `Give it another \"path\", or mount \"${app}\" elsewhere (--mount ${app}=/<segment>).`,\n ]\n : [];\n });\n}\n\n/**\n * Every route, one line each, as `mistri check --routes` prints it.\n * `home` is the App's home as the browser shows it (mount.ts `appHome`):\n * `/` at the root, `/compass` under its id, `/tools/` under a base path.\n */\nexport function routeTable(book: AddressBook, home: string): Array<{ path: string; what: string }> {\n const rows: Array<{ path: string; what: string }> = [{ path: home, what: 'home' }];\n const under = home.replace(/\\/$/, '');\n const records = [...book.records.values()];\n for (const [segment, target] of book.routes) {\n const at = `${under}/${segment}`;\n const what = describe(target);\n if (target.kind === 'dashboard') {\n /* The route itself, then one line per parameter it reads, each said\n only where the dashboard offers it: a room's place, a window, a pick. */\n rows.push({ path: at, what });\n if (target.params)\n rows.push({ path: `${at}?${target.params.map((name) => `${name}=…`).join('&')}`, what: `${what}, at the place it keeps` });\n if (target.window) rows.push({ path: `${at}?period=…`, what: `${what}, over one of its windows` });\n for (const column of target.picks ?? []) rows.push({ path: `${at}?${column}=…`, what: `${what}, picked by \"${column}\"` });\n for (const key of target.periods ?? [])\n rows.push({ path: `${at}?period.${key}=…`, what: `${what}, the section \"${key}\"'s own window` });\n continue;\n }\n rows.push({ path: at, what });\n if (target.kind === 'list') {\n if (target.tabs) rows.push({ path: `${at}?tab=…`, what: `${what}, on one of its tabs (${target.tabs.join(', ')})` });\n if (target.filters) rows.push({ path: `${at}?{filter}=…`, what: `${what}, refined by its filters (${target.filters.join(', ')})` });\n }\n if (target.kind === 'list' || target.kind === 'table') rows.push({ path: `${at}/new`, what: `the New form over \"${target.table}\"` });\n for (const route of records.filter((route) => route.segment === segment)) {\n const table = book.tables.get(route.table)!;\n const id = route.column\n ? `{${route.column}}`\n : route.parent\n ? undefined\n : primaryKeyOf(table).length === 1\n ? `{${primaryKeyOf(table)[0]}}${route.slug ? '[-slug]' : ''}`\n : primaryKeyOf(table)\n .map((column) => `${column}={…}`)\n .join(',');\n if (id) rows.push({ path: `${at}/${id}`, what: `one record of \"${table.name}\"` });\n if (route.parent) {\n const parent = book.records.get(route.parent.table)!;\n rows.push({\n path: `${under}/${parent.segment}/{${route.parent.column}}/${segment}/{${route.parent.child}}`,\n what: `one record of \"${table.name}\", under its \"${book.tables.get(parent.table)!.name}\"`,\n });\n }\n }\n }\n return rows;\n}\n","/**\n * The App model: the internal shape every server and client reader works\n * from (AppDef and its parts) and the enumerations the parsers and the JSON\n * schema share. The helpers every parse location uses are model.ts's, and a\n * list's presentations listPresentations.ts's: the page and list rules read\n * them too, and those rules are not the App's.\n *\n * The parse front in app.ts, with appResourceBlocks.ts, appAuthGrants.ts,\n * appViews.ts, appHome.ts, appPages.ts and appUi.ts, validates the authored\n * object and normalises it into AppDef. The data half speaks the authored\n * nouns — `sources` with their default, and one ResourceDef per declared\n * resource under its logical name, holding its New form, its actions and its\n * captions; list and dashboard views keyed by id, and the nav tree of\n * references to them. Nothing here imports another app module: they all\n * import this one.\n */\nimport type { SectionWidth } from '@mistriwork/engine/shared/page.js';\nimport type { FormRule, FormStep } from './formRules.js';\nimport type { FormStyle } from '@mistriwork/engine/shared/formValues.js';\nimport type { ThemeDefinition } from '@mistriwork/engine/shared/theme.js';\nimport type { RecordBrandingSpec } from './branding.js';\nimport type { TableSizing } from '@mistriwork/engine/shared/tableSizing.js';\nimport type { AppAction, AppFormCommandReference } from './command.js';\nimport type { AppIconName } from '@mistriwork/engine/shared/appIcons.js';\nimport type { SaidCaptions } from '@mistriwork/engine/shared/appMessages.js';\nimport { CHART_SHAPES, type AppNoun, type FigureFormat } from '@mistriwork/engine/shared/model.js';\nimport type { AppListEmpty, AppListPresentation, AppListTab, FilterBar } from '@mistriwork/engine/shared/listPresentations.js';\nimport type { FunnelStage } from '@mistriwork/engine/shared/funnel.js';\nexport type { FormScalar, FormCondition, FormRule, FormChildGroup, FormStep, AppFormField } from './formRules.js';\nexport type {\n AppAction,\n AppUpdateAction,\n AppCommandAction,\n AppCommandReference,\n AppCommandField,\n AppFormCommandReference,\n} from './command.js';\nimport {\n isServedForm,\n type AppDisplayRule,\n type AppFileDef,\n type AppFirstRunStep,\n type AppForm,\n type AppLanding,\n type AppShell,\n type AppTagsDef,\n type LandingFooter,\n type LandingImage,\n type SeriesColor,\n type ServedDashboardView,\n type ServedFormView,\n} from '@mistriwork/engine/shared/servedApp.js';\nexport type {\n AppAvatarPhoto,\n AppDisplayRule,\n AppFileBucket,\n AppFileDef,\n AppFirstRunStep,\n AppForm,\n AppLanding,\n AppShell,\n AppShellNew,\n AppSidebar,\n AppTagsDef,\n FormLines,\n LandingAction,\n LandingActionIntent,\n LandingFooter,\n LandingBadge,\n LandingHero,\n LandingHeroLayout,\n LandingPoint,\n LandingImage,\n LandingItem,\n LandingLink,\n LandingQuestion,\n LandingSearch,\n LandingSection,\n SeriesColor,\n} from '@mistriwork/engine/shared/servedApp.js';\nexport { navViewKeys, PLACEMENTS, viewOrder } from '@mistriwork/engine/shared/servedApp.js';\n\n/**\n * A list view: the rows of one resource — how they read (`show`, `tabs`,\n * `filters`), how they draw (`presentation`), how they open (`open`). Keyed\n * in `AppDef.views` by its id, which is also its address: the client keys a\n * room on the view it was opened from, and the address carries it before\n * the list's question (`/{app}/{id}:{query}`) — how a split beside a plain\n * list over one table is told apart. What a person may do with the rows —\n * the New form, the actions — is the resource's (`ResourceDef`).\n */\nexport interface AppListView {\n /** The table's id — qualified where the engine qualifies things. */\n table: string;\n /** What the list is called. Absent means the table's own name, humanised. */\n label?: string;\n /** Its address after the App's, when not its key (appAddress.ts). */\n path?: string;\n /** The menu's mark for it, one of the App icons (appIcons.ts). */\n icon?: AppIconName;\n /** With the platform realm, whose context the list serves; its menu group's by default. */\n realm?: AppRealm;\n /** The roles that may open it, beside its table grant (appViewRoles.ts). Absent is everyone's. */\n roles?: string[];\n /**\n * A module: conditions this view always stands on — \"is_urgent = true\"\n * makes the view \"Urgent orders\", a destination of its own beside the\n * plain \"Work orders\". Tabs refine on top of it. Conditions only; the\n * shown columns stay the view's `show`.\n */\n query?: string;\n /** Columns or label paths the list shows, in order. Absent means all of them. */\n show?: string[];\n /** A split's single keys, key → the id of an action of the picked record. */\n keys?: Record<string, string>;\n /** false draws the list without its title. */\n heading?: false;\n presentation?: AppListPresentation;\n /** Copy for the list as it opens (its first tab) with no rows; a tab says its own (`tabs[].empty`), and a search or\n filter past the tab keeps the no-matches message. */\n empty?: AppListEmpty;\n /** Filter tabs across the top of the list. Absent means one unfiltered list. */\n tabs?: AppListTab[];\n /**\n * Served, never written: what the menu's count counts where it is not\n * `query`. A list over a saved form's root whose tab shows the drafts keeps\n * them out of every other tab, and out of its count too (savedFormLists.ts).\n */\n countQuery?: string;\n /** The menu's count (ui.shell.counts): true counts this list, false never does; absent, the rule\n decides (shared/menuCounts.ts). */\n count?: boolean;\n /**\n * Columns offered as quick filters above the list, in order. Absent means\n * the client derives them — enum-vocabulary columns and boolean flags.\n * An empty list means none, on purpose.\n */\n filters?: string[];\n /** 'none' hides this list's filter bar; tabs remain visible. 'rail' draws the\n filters as a rail of facets beside the rows instead (a sheet on a phone). Absent: 'bar'. */\n filterBar?: FilterBar;\n /** A text filter's suggestions as the reader types: the distinct values of these columns of the\n list's own resource, under the list's standing query and the reader's grants (SuggestBox.tsx). */\n suggest?: Record<string, string[]>;\n /** Opt in to the raw query editor and Advanced filter action. Hidden by default. */\n advancedFilters?: boolean;\n /** Inline the resource's New form, subject to the ordinary create gates. */\n quickAdd?: true;\n /** The list's own CSV import, or false for none where the resource offers one (appImport.ts). */\n import?: import('./appImportDeclaration.js').AppImportDef | false;\n /** Whether the view carries a Favorites star. Opt-in. */\n favorite?: true;\n /** Numeric columns summed in a footer row over the whole query, not the page. A grid only. */\n totals?: string[];\n /** Row height; 'comfortable' is the default. */\n density?: 'comfortable' | 'compact';\n /** A ⋯ menu at the end of each row with the record's own actions. A grid only. */\n rowActions?: true;\n /** Cells edited in place, and a selection's \"Set field…\": every shown\n column the reader may write (`true`), or the columns named. A grid only. */\n editable?: true | string[];\n /** How this list's rows open: the full page (default), a drawer over\n the list, or a modal, centred; both glanced and dismissed, the page\n one click away. */\n open?: (typeof OPEN_MODES)[number];\n /** A reference column whose record a row opens instead of the row: a join table's list opens what each row is about. A grid only. */\n opens?: string;\n sizing?: TableSizing;\n /**\n * A queue's or a board's selector: the ids of its resource's actions\n * it offers, in this order. Absent means every one of them. The actions\n * themselves are the resource's (`ResourceDef.actions`).\n */\n actions?: string[];\n /** The orders a reader may choose, in a Sort menu beside the filters: each a label and a sort\n written like the query bar (\"price\", \"price desc\", \"year desc, mileage\"). Absent: no menu. */\n sorts?: Array<{ label: string; sort: string }>;\n /** The order the list reads in when nothing else says one, written like the query bar: a tab's own sort\n wins over it, and the Sort menu over both. Kept out of the bar, as a hand order is. Not beside `order`. */\n sort?: string;\n /**\n * The numeric column holding each row's place: the list reads in it,\n * ascending, wherever nothing else sorts the question, and a reader who\n * may edit it puts rows in place — a drag, Alt+↑/↓, or the row's grip.\n * A grid, a checklist, a queue or cards; a board keeps its own in its\n * presentation.\n */\n order?: string;\n}\n\n/**\n * One node of nav, as authored: a reference to a view — with the menu's own\n * word for it when it has one — or a group of nodes. Nav only arranges; the\n * views it names are `AppDef.views`.\n */\nexport type AppNavNode =\n | { view: string; label?: string; color?: SeriesColor }\n | { label: string; to: import('../../schema/app.js').StaticAppTarget; color?: SeriesColor }\n | {\n group: string;\n label: string;\n icon?: AppIconName;\n realm?: AppRealm;\n placement?: 'footer';\n inline?: true;\n folded?: true;\n children: AppNavNode[];\n };\n\nexport const AUTH_PROVIDERS = ['email', 'github', 'google'] as const;\n/** An identity provider the deployment's config names (`oidc('workforce')` is `'oidc:workforce'`, `saml('corp')`\n `'saml:corp'`), or `'oidc:*'` / `'saml:*'`: each tenant's own provider of that kind, found by the email's\n domain, in a tenant App. */\ntype OidcProvider = `oidc:${string}`;\ntype SamlProvider = `saml:${string}`;\nexport type AppProvider = (typeof AUTH_PROVIDERS)[number] | OidcProvider | SamlProvider;\n/** The name after `oidc:`: a config entry's name, lower-case, never one of the built-in providers'. */\nexport const OIDC_PROVIDER = /^oidc:([a-z][a-z0-9-]{0,39})$/;\nexport const TENANT_OIDC = 'oidc:*';\n/** The name after `saml:`, as for `oidc:`. One namespace: an oidc and a saml entry never share a name. */\nexport const SAML_PROVIDER = /^saml:([a-z][a-z0-9-]{0,39})$/;\nexport const TENANT_SAML = 'saml:*';\n/** A provider the config file names, either kind: the name after the colon. */\nexport const NAMED_PROVIDER = /^(?:oidc|saml):([a-z][a-z0-9-]{0,39})$/;\n/** Each tenant's own providers, either kind. */\nexport const TENANT_PROVIDERS: readonly string[] = [TENANT_OIDC, TENANT_SAML];\nexport const SIGN_UP_MODES = ['open', 'invite'] as const;\n/** Just-in-time sign-up: an account is made at its first sign-in through one of these providers, or with a\n verified email in one of these domains, or both when both are said. */\ninterface AppJustInTime {\n providers?: AppProvider[];\n domains?: string[];\n}\nexport type AppSignUp = (typeof SIGN_UP_MODES)[number] | AppJustInTime;\n/** A role an identity provider's claims give: a claim (a name, or a dotted path into one) and the values that\n mean the role, in `match`. Any listed value of any listed claim gives it. With `provider` (authored\n `claims: { provider: oidc('workforce'), … }`), only a sign-in through that provider reads it; without, any\n of the App's named providers does. */\nexport interface AppClaimRule {\n provider?: AppProvider;\n match: Record<string, string[]>;\n}\n/** Row scoping within the role grants: every row, a server-defined row policy, or tenant membership. */\nexport const ROW_SCOPES = ['all', 'policy', 'tenant'] as const;\nexport type AppRows = (typeof ROW_SCOPES)[number];\n\n/** Whose context something belongs to in a tenant App with the platform realm\n (`auth.tenancy.platform`): a tenant's own, or the platform realm's, which\n reads across tenants. Absent means the App's own realm, `tenant`. */\nconst REALMS = ['tenant', 'platform'] as const;\nexport type AppRealm = (typeof REALMS)[number];\nexport function parseRealm(value: unknown, at: string): AppRealm {\n if (value !== 'tenant' && value !== 'platform') throw new Error(`${at} has a \"realm\" of \"${String(value)}\". Use \"tenant\" or \"platform\".`);\n return value;\n}\n/** Whose rows a tenant App's resource holds (`auth.rows: 'tenant'`): one\n tenant's, every tenant's read-only (`global`), or the platform realm's. */\nconst RESOURCE_OWNERS = ['tenant', 'global', 'platform'] as const;\ntype ResourceOwner = (typeof RESOURCE_OWNERS)[number];\n/** What a tenant clone copies of a tenant-owned resource (`clone`): its rows\n always (`config`), or only when the clone asks for samples (`sample`). */\nexport const RESOURCE_CLONES = ['config', 'sample'] as const;\nexport type ResourceClone = (typeof RESOURCE_CLONES)[number];\n/** `auth.tenancy`: what a tenant is called, the platform's table of tenants,\n and the platform realm. */\nexport interface AppTenancy {\n /** What one tenant is called in the UI (\"Dealer\"). Default \"Tenant\". */\n label?: string;\n /** The platform-owned resource with one row per tenant: the one platform\n resource a tenant row may reference, and only by its ownership column. */\n profile?: string;\n /** Declares the platform realm; its label names the platform context (\"Lender\"). `me` is who the word is there,\n as `auth.me` says it in a tenant's: a row of the platform's table of its staff, by the column holding their\n account id. Absent, `me` in the platform realm is nobody's row. */\n platform?: { label: string; me?: { from: string; column: string } };\n}\n\n/** How people sign in: what the auth runtime captures at startup, so changing it needs a restart. */\nexport interface AppSignIn {\n /** How people sign in: email + password, and the social providers whose\n client id and secret the environment holds. Default: email. */\n providers: AppProvider[];\n /** `invite`: an admin creates accounts; `open`: anyone may sign up; `{ providers, domains }`: an account\n is made at its first sign-in through an identity provider, within those. Default: invite. */\n signUp: AppSignUp;\n /** The SQLite file sessions and accounts live in. Default: the mistri\n config directory, named for the app — never the app's own database and\n never the app directory, which is committed. */\n store?: string;\n /** The role a new account gets. Default: user. */\n defaultRole: string;\n}\n\n/**\n * `auth`: sign-in, and who may do what once signed in. Its grants are made\n * within the resource ceilings (`ResourceDef.access` and `fields`): one\n * ceiling model, with or without sign-in.\n */\nexport interface AppAuth extends AppSignIn {\n /** Row scoping within the grants. Default: all. */\n rows: AppRows;\n /** Default-deny grants within the resource ceilings, by role. Absent: a\n signed-in App with no grants, which scopes nothing. */\n roles?: Record<string, AppRoleGrant>;\n /** What a signed-out visitor reads (`auth.public`): these resources, read-only,\n narrowed by `fields`. Absent: a signed-out visitor opens only the landing\n and public forms. */\n public?: AppPublicGrant;\n /** The human check every public door asks first (`auth.challenge`): a verifier the commands module's `challenges`\n names (commands/challenges.ts). Absent: the doors rely on their ticket, honeypot and budgets. */\n challenge?: string;\n /** Tenancy (`auth.rows: 'tenant'`): what a tenant is called, its profile, and\n the platform realm. Absent: tenants only, no platform realm. */\n tenancy?: AppTenancy;\n /** Who `me` is as a row (appMe.ts): the table people are rows of, and its\n column holding their sign-in email — in a tenant App, their account's id, one row per membership.\n Absent, `me` is the email alone. */\n me?: { from: string; column: string; create?: { name?: string } };\n}\n\n/** A signed-out visitor's reads: every resource named, never `true`, and nothing written. */\nexport interface AppPublicGrant {\n read: string[];\n /** A narrowing of the column ceiling for signed-out visitors; reads only. */\n fields?: Record<string, AppFieldAccess>;\n}\n\nexport interface AppRoleGrant {\n /** The role's name as people read it, wherever a screen names the role (accounts, grants, invitations); the id otherwise. */\n label?: string;\n /** The role whose grants it holds too. Its grants below already include them (appAuthGrants.ts inheritedGrants); the\n name says where they came from. */\n extends?: string;\n flags?: readonly import('./appFlagCapabilities.js').FlagCapability[];\n experiments?: readonly import('./appFlagCapabilities.js').ExperimentCapability[];\n /** true grants the App's ceiling, never every database resource. */\n read: boolean | string[];\n write: boolean | string[];\n /** Of what it writes, what the role makes new rows in: absent, all of it;\n false, none (it edits and never creates); a list, those. */\n create?: false | string[];\n /** Tables the role may export (a CSV/JSON file of every matching row) beyond those it writes, which it may always\n export. Of what it reads. Absent: only what it writes. */\n export?: string[];\n /** Explicit server command IDs only. Absent denies; never grants generic CRUD. */\n commands?: string[];\n /** Fixed actions this role may run on a resource it reads and does not\n write, by id (`{ pay_run: ['submit'] }`). The action's own\n `set`, `when` and reason are the whole of the write; never generic CRUD. */\n actions?: Record<string, string[]>;\n /** Operator reads this role may make: readiness, the support bundle and/or the durable workflows room. Absent denies. */\n operations?: OperationsGrantAction[];\n /** A narrowing of the column ceiling for this role. */\n fields?: Record<string, AppRoleFieldAccess>;\n /** With the platform realm, whose context the role holds: a tenant's or the\n platform's. Required on every role then; roles of the two realms never\n combine. A platform role edits only the platform's own writable rows\n (appTenancy.ts) and changes a tenant's through commands. */\n realm?: AppRealm;\n /** Authored as `operations: ['support']` on a tenant role: it may read its own dealer's support sessions: who\n entered, why, when, how it ended, and the field changes this role's own\n grants already let it read. Never the operator's email, the native\n session, a version or a MAC. Declared, never defaulted. */\n supportLog?: true;\n /** Authored as `operations: ['support']` on a platform role: it may read every dealer's support sessions, the one\n view where the operator's email appears. Declared, never defaulted. */\n supportAudit?: true;\n /** The identity provider's claims that give this role, re-read at every sign-in through an `oidc:` provider\n (auth/identityRoles.ts). A role with it is the provider's to give and take away. */\n claims?: AppClaimRule;\n /** A role only a service principal holds — a jobs module's principal, an App API token — never a person: a\n free-form page is offered to it only where the page names it, and it is never the default role or a\n claim's. Declared, never inferred: the jobs module is read after the App. */\n service?: true;\n}\n\nexport interface AppFieldAccess {\n /** Local column names, not reference paths. Complete primary keys stay readable. */\n read: string[];\n write?: string[];\n}\n/** A role's narrowing: `read` absent is the resource's own read ceiling, so the role narrows only `write`. */\nexport type AppRoleFieldAccess = Omit<AppFieldAccess, 'read'> & { read?: string[] };\n\nexport const OPERATIONS_GRANT_ACTIONS = ['readiness', 'support', 'workflows', 'jobs', 'deliveries', 'stores', 'analytics'] as const;\nexport type OperationsGrantAction = (typeof OPERATIONS_GRANT_ACTIONS)[number];\n\n/** The sign-in half of `auth`, without its grants: what the auth runtime\n captures, and so what a reload may not change. Comparisons normalise\n both sides with it, so a runtime's captured settings and the App's\n `auth` are judged on the same four facts. */\nexport function signInOf(auth: AppSignIn): AppSignIn;\nexport function signInOf(auth: AppSignIn | undefined): AppSignIn | undefined;\nexport function signInOf(auth: AppSignIn | undefined): AppSignIn | undefined {\n if (!auth) return undefined;\n return {\n providers: auth.providers,\n signUp: auth.signUp,\n ...(auth.store === undefined ? {} : { store: auth.store }),\n defaultRole: auth.defaultRole,\n };\n}\n\n/**\n * A command form view: one form running one command that creates records,\n * keyed in `views` like every view. With steps, every command input\n * belongs to exactly one; without, the form is one page. Presentation only\n * — the handler, registered through `--commands`, owns validation and\n * every write.\n *\n * Signed in unless `public` is set: a room of the App, run through the\n * signed-in grant and served only to a role granted its command. Public: a\n * page at `/{app}/form/{key}` for anyone, run as one public command under\n * a public principal, even for someone signed in.\n */\nexport interface AppFormView<Field extends string = string> extends ServedFormView {\n /** The form's heading. */\n label: string;\n /** A signed-in form's address after the App's, when not its key. */\n path?: string;\n /** The menu's mark for a signed-in form; a public form is in no menu. */\n icon?: AppIconName;\n /** With the platform realm, whose context the signed-in form serves. */\n realm?: AppRealm;\n /** A signed-in form's: the roles that may open it, beside its command grant (appViewRoles.ts). */\n roles?: string[];\n /** Shown under the heading. */\n description?: string;\n /** The command the form runs: on a form saved each step (`save`), the final submit. */\n command: AppFormCommandReference;\n /** On a saved form, each step runs its own command and asks that command's inputs. */\n steps?: Array<FormStep<Field> & { command?: AppFormCommandReference }>;\n submitLabel?: string;\n /** How the steps are drawn: the stepper (default) or the intake style (a progress bar, one question at a time, a\n sticky bar of actions and an editable review). */\n style?: FormStyle;\n /** Intake: a quiet line over the progress bar. */\n note?: { icon?: AppIconName; text: string };\n /** Intake: whether the last page reviews every answer. Present only as `false`; absent, an intake form reviews. */\n review?: false;\n /** The screen after the receipt, beside the reference the handler returned when it returned one. */\n done?: { title?: string; description?: string };\n public?: true;\n /** The inputs a link's address may start filled (`?stock_ref=FC-1049`): shown, editable, checked like anything typed. */\n prefillable?: string[];\n /** Command input → the App's own input component, drawn in place of the platform's control. Never on a public form. */\n inputs?: Record<string, import('@mistriwork/engine/shared/appComponents.js').ComponentDecl>;\n /** Saved on every step into the App's real tables (appSavedForms.ts): a public form only. */\n save?: import('./appSavedForms.js').AppFormSave;\n /** A saved form's status check: a receipt confirmed by what only the applicant knows, answering a state. */\n statusCheck?: import('./appSavedForms.js').AppStatusCheck;\n /** Linking a submitted application to an account on proven control (appSavedForms.ts, forms/formLink.ts). */\n link?: import('./appSavedForms.js').AppFormLink;\n}\n\n/** A form view as served, carrying its reason when its reference inputs do not fit this catalog. */\nexport type ResolvedFormView = AppFormView & { error?: string };\n\n/** Whether a view is a command form: the one kind of view that runs a command. */\nexport const isFormView: (view: object) => view is AppFormView = isServedForm as (view: object) => view is AppFormView;\n\n/** Whether a view is a public form: a form that says so, and nothing else. */\nexport const isPublicForm = (view: object | undefined): view is AppFormView & { public: true } =>\n view !== undefined && isFormView(view) && view.public === true;\n\n/** Whether a view is a public free-form page that runs public commands: a door a signed-out visitor submits through, as a\n public form is one (http/pageCommandRoutes.ts). */\nexport const isPublicCommandPage = (view: object | undefined): view is { page: { public: true; commands: readonly string[] } } => {\n const page = (view as { page?: { public?: unknown; commands?: unknown } } | undefined)?.page;\n return page?.public === true && Array.isArray(page.commands) && page.commands.length > 0;\n};\n\nexport const LANDING_SECTION_KINDS = ['features', 'steps', 'faq', 'text', 'callToAction', 'contact', 'view', 'search'] as const;\nexport const LANDING_ACTION_INTENTS = ['primary', 'secondary'] as const;\nexport const LANDING_HERO_LAYOUTS = ['split', 'overlay'] as const;\n/** Every ceiling the landing parser holds a string or a list to, in one place. */\nexport const LANDING_LIMITS = {\n title: 120,\n heading: 120,\n text: 600,\n prose: 2000,\n label: 60,\n question: 200,\n answer: 1200,\n href: 2048,\n actions: 3,\n sections: 12,\n items: 6,\n questions: 12,\n links: 8,\n shown: 12,\n filters: 6,\n highlights: 4,\n highlight: 60,\n alt: 200,\n badge: 40,\n tiles: 4,\n points: 4,\n} as const;\n\n/**\n * The app's look, as overrides of the design tokens the UI already runs\n * on — a whitelist, not a stylesheet: an app may say what its accent is,\n * how round its corners are, and what face its data wears; it may not\n * supply a registered preset for broader visual roles. The legacy tokens\n * remain supported without requiring a full definition. The soft tint is\n * derived from `accent`, so one colour — or one for each ground,\n * `{ light, dark }` — is a complete theme.\n * `font` is the *data* face and the reader's typography preference\n * still wins over it — a brand is not a reason to overrule someone's eyes.\n */\nexport interface AppTheme {\n preset?: string;\n palette?: string;\n accent?: import('@mistriwork/engine/shared/themePresets.js').AppAccent;\n /** Corner radius for the whole UI, a CSS length: \"0\", \"10px\". */\n radius?: string;\n /** The data face, a CSS font stack. Code surfaces keep the code face. */\n font?: string;\n /** A theme per audience (themeResolve.ts): `public` for a signed-out visitor's pages, and one per role. */\n for?: import('@mistriwork/engine/shared/themeResolve.js').ThemeAudiences;\n /** Never authored: the server marks the look it tells a signed-out visitor (appLook), which the reader's pick does\n not move, so a public page is the same for everyone. */\n audience?: 'public';\n}\n\nexport const THEME_KEYS = ['preset', 'palette', 'accent', 'radius', 'font', 'for'] as const;\n\n/**\n * One section of a dashboard: a rootless metric, breakdown or rows of a table, narrowed by a\n * query, laid out with the page sections' widths. What a page composes\n * about one record, a dashboard composes about the whole table.\n */\n/** A metric's goal read from data: a measure over a table and a query, the metric's own three words.\n Resolved: `table` is the table id (the metric's when unwritten), `measure` the metric's when unwritten. */\nexport interface DashTarget {\n table: string;\n query?: string;\n measure: string;\n /** A breakdown's: the target table's column whose value matches each group's key (G76). */\n by?: string;\n /** A breakdown's: the target table's date column the reader's window narrows, as the section's `over`. */\n over?: string;\n /** A whole (`targetWhole`): the goal's query without the author's own window phrase, so the reader's window replaces\n it as it does the metric's (dashboardAsks.ts, windowed). */\n windowBase?: string;\n}\n\nexport interface AppDashSection {\n sizing?: TableSizing;\n /** A rows section's guidance when it has no row: what belongs there, and where to start. */\n empty?: AppListEmpty;\n /** Authored `empty: 'hide'` (a metric takes none): the section is left out of the dashboard while it has nothing. */\n emptyMode?: 'hide';\n kind:\n | 'metric'\n | 'breakdown'\n | 'rows'\n | 'chart'\n | 'note'\n | 'matrix'\n | 'funnel'\n | 'board'\n | 'firstRun'\n | 'custom'\n | 'activity'\n | 'find'\n | 'branding'\n | 'integrations';\n /** find: the resources one box searches by their records' names (config `resources`), served as table ids. */\n searched?: string[];\n /** find: the box's words before anything is typed. */\n placeholder?: string;\n /** activity: the resources whose record history the feed reads, each with its mark and, where the\n resource's noun is not what the feed should call it, its word. Each keeps a history, as a page's\n `history()` makes its resource keep one. */\n resources?: Record<string, AppActivityResource>;\n /** custom: the author's component (appComponents.ts). */\n component?: import('@mistriwork/engine/shared/appComponents.js').ComponentDecl;\n /** custom: its named reads of rows (config `rows`), each a rows section's words. */\n reads?: Record<string, import('@mistriwork/engine/shared/appComponents.js').DashCustomRead>;\n /** custom: its named aggregate reads (config `totals`), each a metric or breakdown in all but drawing. */\n aggregates?: Record<string, import('@mistriwork/engine/shared/appComponents.js').DashCustomTotal>;\n /** custom: its props, checked against the declaration, defaults filled. */\n props?: Record<string, unknown>;\n title?: string;\n /** firstRun: what a new App asks of its first reader, in order — each a list view to add the first\n record to. Served with each view's table, which says whether the step is done (it has a row). */\n steps?: AppFirstRunStep[];\n /** Every kind but a note is about a table. */\n table?: string;\n /** note: the words. Plain text; blank lines separate paragraphs. */\n text?: string;\n /** A curated mark: above a note's guidance, or in any other section's\n heading row, beside its title (a metric's label, a chart's, a list's). */\n icon?: AppIconName;\n /** note: one link to one of the App's rooms: a list, a dashboard or a signed-in form view (appDestinations.ts). */\n action?: import('@mistriwork/engine/shared/appLinks.js').AppLinkAction;\n /** note: 'banner', the dashboard's introduction, dressed by the look's `banner` recipe. */\n variant?: 'banner';\n /** note, a banner: its short tag over the title. */\n eyebrow?: string;\n /** note, a banner: its picture, one row of a file resource. */\n image?: LandingImage;\n /** The query language, without the leading table name. */\n query?: string;\n /** metric: the breakdown grammar's words — \"count\", \"sum amount as pipeline\". */\n measure?: string;\n /** metric: a second query for the same measure; the card shows the delta. Unsaid with an `over`, the period before\n the window (app.ts, `followsClock`). */\n compare?: string;\n /** metric: which way is good news, so the delta can say so — a fall in\n \"won\" is bad, a fall in \"overdue\" is good, and the card cannot know\n without being told. Absent means the delta stays neutral. */\n better?: 'up' | 'down';\n /** metric: draw the measure's last twelve months inside the card while no window stands. Needs `over`. A windowed\n metric draws its trend across the window unless this is false (dashboardTrend.ts, sparkText). */\n spark?: boolean;\n /** metric, breakdown, chart and matrix: how its figures are said where the measure is not one column's (a computed\n or averaged one has no display rule): percent — the figure is 0–100, said with % — number, money in the\n App's currency, or duration — the figure is seconds, said as \"10h 2m\". */\n format?: FigureFormat;\n /** metric, breakdown, chart and matrix: a word said after each figure (\"days\", \"/mo\"). */\n unit?: string;\n /** metric: money said exact, to the currency's minor digits at any size (the default: a balance is read to the\n cent), or `compact` from a thousand (\"$28.1K\"), for a revenue card glanced at. */\n figure?: 'exact' | 'compact';\n /** metric and chart: a goal — a meter and the share reached on a card, a\n rule in the ink on a line or stacked bars. Never on a donut, which has\n no scale to rule. A metric's goal may be read from data: a second\n measure, said in the metric's own words (DashTarget). */\n target?: number | DashTarget;\n /** A metric whose data `target` counts the same measure over the same table with no more conditions: the target is\n the whole this figure is a share of, said \"118 of 159 (74%)\" rather than as a goal (resolveDashboard). */\n targetWhole?: true;\n /** breakdown/chart: what to group by — \"status\", \"month opened_on\". A chart may say \"auto created_at\": the unit then\n follows the window and the data (autoBucket.ts). */\n by?: string;\n /** funnel: the stages a row passes in order, 2 to 8 (funnel.ts). */\n stages?: FunnelStage[];\n /** board: ordered subset of exact lane values (1..8). Omit to discover lanes. */\n lanes?: string[];\n /** chart: how it draws. A line wants a bucketed by; bars, a bucketed by or\n categories, one series with the current bar marked; a donut, categories;\n stacked bars, a two-key by — a bar per group of the first, divided by the second.\n scatter: a dot per group of a one-key by, placed by `x` across and `measure` up; heatmap: a two-key by\n as a grid of cells shaded by the measure; waterfall: a one-key by's values as rises and falls from zero to\n a total; treemap: a one-key by's groups as tiles sized by the measure.\n breakdown: \"bars\" (the default), or \"steps\": a card per value of the column, in its order, joined by arrows. */\n shape?: 'line' | 'bar' | 'donut' | 'stacked' | 'scatter' | 'heatmap' | 'waterfall' | 'treemap' | 'bars' | 'steps';\n /** chart, a scatter: the measure that places each dot across, in the breakdown grammar's words (\"count\"). */\n x?: string;\n /** breakdown, steps: a line under a step, by the value it counts. */\n notes?: Record<string, string>;\n /** chart, a line: \"smooth\" draws a monotone curve through the points (it never overshoots one). */\n curve?: 'smooth' | 'straight';\n /** chart, a donut: what the figure in its middle counts — \"Total visitors\" — said under it. */\n total?: string;\n /** chart: the drawing's height in CSS pixels (120 or more). Absent means\n a third of the width, capped — a sparse series on a wide screen is\n better short. */\n height?: number;\n /** The date column the dashboard's time window narrows this section by.\n Absent means the window passes this section over. */\n over?: string;\n /** The section's own windows, in the query grammar's words: the reader picks this section's period in\n its heading, apart from the dashboard's, and the first is where it starts. Needs `over`. */\n windows?: string[];\n /** Served, never authored: the section's name in the address when it has its own windows —\n `?period.lines-moved-by-day=last-7-days` — the slug of its title, one per dashboard. */\n windowKey?: string;\n /** An embed: the list works as its room does — its search, its filters and, for a reader offered one,\n its selection and bulk actions — rather than drawing read-only. */\n interactive?: true;\n /**\n * Served, never authored: the query with its own `over = last N …` phrase\n * removed, for the reader's window to stand in its place. A reader picking\n * \"last 12 months\" on a metric authored `close_date = last 90 days` means\n * twelve months, not the ninety days the two windows have in common.\n */\n windowBase?: string;\n /**\n * Served, never authored: the compare was derived from \"previous period\",\n * so the client may re-derive it against the reader's window — `last 30\n * days` picked means `previous 30 days` compared — instead of comparing the\n * reader's window with the author's.\n */\n comparePeriod?: boolean;\n /** rows: columns, label paths welcome. */\n show?: string[];\n /** rows: column path → the app's word for it, for the show entries that\n carried a label. Served beside `show`, which stays the bare paths the\n query is built from. */\n labels?: Record<string, string>;\n /** rows: query-bar text. */\n sort?: string;\n limit?: number;\n width?: SectionWidth;\n /** The area of the dashboard's `layout` this section stands in. Absent without a layout; every section names one with it. */\n area?: string;\n /** An embed (`{ kind: 'view' }`): the key of the list view this section\n draws, whose room its \"open →\" opens. A metric: the key of the list view\n over its own table that the card opens, narrowed by the metric's query. A breakdown, chart, matrix or funnel: the list its\n groups (a funnel's stages) open (`to`); absent, the one list over its table that the reader is served. Absent on every other section. */\n view?: string;\n /** A metric, breakdown, chart, matrix or funnel whose authored `to` is false: its card or its groups are text, not doorways. */\n noDrill?: true;\n}\n\n/** A view that is a screen, not a table: a named set of sections, keyed in\n `AppDef.views` by its view key. Its address is `dash-<key>`, the page\n token it rides. The payload serves the same shape resolved against a\n catalog (resolveViews): sections with resolved table ids, or a reason. */\nexport interface AppDashboardView extends ServedDashboardView {\n label: string;\n /** Its address after the App's, when not its key. */\n path?: string;\n /** The words over the sections, when not its label (which the menu and the tab keep): \"Hello, {me.name}\".\n `{me.name}` is the signed-in reader's name, said by the server (appGreeting.ts). */\n heading?: string;\n /** `heading: false`: the title is not drawn, and stays for a screen reader. */\n headingHidden?: true;\n /** A short plain-text introduction shown under the dashboard heading; it may greet `{me.name}` too. */\n description?: string;\n /** The menu's mark for it, one of the App icons (appIcons.ts). */\n icon?: AppIconName;\n /** With the platform realm, whose context the dashboard serves; its menu group's by default. */\n realm?: AppRealm;\n /** The roles that may open it (a room's too), beside its tables' grants (appViewRoles.ts). */\n roles?: string[];\n sections: AppDashSection[];\n /** An independent right-hand flow on wide screens; stacks below the main flow when narrow. */\n aside?: AppDashSection[];\n /** The sections placed on a grid: one string per row, each the same 1 to 4 space-separated cells, a cell an area's\n name (a section's `area`) or `.` for none. An area covering cells in two rows spans them; sections in one area stack\n in source order. Never with an `aside`. */\n areas?: string[];\n /** The time windows offered when any section declares `over`, in the\n query grammar's own words. Default: last 30 days / 90 days / 12 months,\n served filled in whenever any section has `over`, so the client never\n re-declares the default list. */\n windows?: string[];\n /** The window the dashboard opens on, one of `windows`; unsaid, it opens on all time. An address's `?period=` wins. */\n window?: string;\n /** How the window is picked: \"segmented\", a button per window (the default), or \"button\", one date-range button —\n the calendar, the dates the window covers, a menu of the windows — where the control row is tight. */\n windowControl?: 'segmented' | 'button';\n /**\n * Pick-filters: column names offered as selectors in the control row.\n * A pick narrows every section whose table has the column — a\n * vocabulary column becomes its values, a whole single-column\n * reference becomes the records it points at, said by name.\n */\n filters?: string[];\n /** Whether the view carries a Favorites star. Opt-in. */\n favorite?: true;\n /**\n * A free-form page (`definePage` in `pages/*.page.tsx`): the one custom\n * section drawing the whole page, the resources it reads — a reader who\n * cannot read one is not served the page — the commands it runs, the\n * address parameters it keeps its place in and the loader that reads for\n * it on the server.\n */\n page?: AppPageManifest;\n /** Served, never authored: the first problem, for the screen; `errors` has all of them. */\n error?: string;\n errors?: string[];\n}\n\n/** What a free-form page declares it reads, runs, keeps in its address and loads: its manifest, held to on both sides. */\ninterface AppPageManifest {\n reads: string[];\n commands: string[];\n params: string[];\n loader?: string;\n public?: true;\n}\n\n/** One resource an activity section reads: its mark, and its word in the feed where not its noun. */\nexport interface AppActivityResource {\n icon?: AppIconName;\n label?: string;\n}\n\nexport const DISPLAY_FORMATS = [\n 'money',\n 'percent',\n 'number',\n 'relative',\n 'date',\n 'datetime',\n 'items',\n 'duration',\n 'email',\n 'phone',\n 'url',\n 'richText',\n 'avatars',\n 'rating',\n 'color',\n 'time',\n 'month',\n 'year',\n 'json',\n 'qr',\n] as const;\n\n/** The roles an App names for people (`auth.roles.<id>.label`), for the sign-in answer: absent where none is labeled. */\nexport function roleLabelsOf(auth: { roles?: Record<string, { label?: string }> } | undefined): { roleLabels?: Record<string, string> } {\n const labeled = Object.entries(auth?.roles ?? {}).flatMap(([role, grant]) =>\n typeof grant?.label === 'string' ? [[role, grant.label] as const] : [],\n );\n return labeled.length ? { roleLabels: Object.fromEntries(labeled) } : {};\n}\n\n/** Whether the App names roles at all: a refusal says \"your role\" only where one could be the reason. */\nexport function keepsRoles(auth: { roles?: Record<string, unknown> } | undefined): { hasRoles?: true } {\n return Object.keys(auth?.roles ?? {}).length ? { hasRoles: true } : {};\n}\n\n/**\n * What an App's source says that the platform would say without it, found\n * where the raw root is still in hand (the parsed App cannot tell a stated\n * default from an implied one). Read by `check` (appWarnings.ts).\n */\nconst restatements = new WeakMap<object, readonly string[]>();\nexport const noteRestated = (app: object, notes: readonly string[]): void => {\n if (notes.length) restatements.set(app, notes);\n};\nexport const restatedOf = (app: object): readonly string[] => restatements.get(app) ?? [];\n\nexport interface AppSourceBinding {\n /** A server-configured connection ID or name, never a connection URL. */\n connection: string;\n /** The source whose resources stay bare. Beside others, at most one says\n so, and none has to; a source that stands alone is it unless it says\n `default: false`, which keeps its `source.name` names as others go. */\n default: boolean;\n /** `'platform'`: a platform-only source, read by a tenant App's platform\n realm alone, never the default and never written (appTenancy.ts). */\n realm?: 'platform';\n /** The one engine schema (a PostgreSQL schema) this source\n sees of its connection. Several sources may share a connection this way;\n each sees its own schema's tables and nothing else. */\n schema?: string;\n /** The tables this source brings into the App without a block each:\n `'all'`, or their local names. Each is an implicit `{}` block, which\n grants nothing a block would not. */\n resources?: 'all' | readonly string[];\n}\n\n/**\n * One logical resource, as its block declares it: where its rows live and\n * what the App says about its values. Keyed in `AppDef.resources` by the\n * logical name — bare for the default source (or the single connection),\n * `source.name` for a secondary source — exactly as authored.\n */\n/**\n * A resource searched through a search provider (server/app/searchProvider.ts):\n * the columns the index holds, and the provider's connection by its name. The\n * index answers keys; rows are always re-read under the reader's own grants.\n */\nexport interface AppSearchDef {\n connection: string;\n fields: string[];\n}\n\n/**\n * What the durable support write trail keeps of a resource's rows. Under a\n * support session (\"enter as dealer\") every Save, New, Delete, Undo and\n * command appends one audit entry in the business write's own transaction; a\n * column named here is recorded as a keyed hash instead of its value, in the\n * change and in a delete's before image alike, and every other column keeps\n * its value. `'values'` marks every column of the resource.\n */\nexport interface AppAuditDef {\n redact: 'values' | string[];\n}\n\nexport interface ResourceDef {\n /** The physical table in its source's catalog, when the name alone does not find it. */\n physical?: string;\n /** The named source the resource lives in; absent for an App without `sources`. */\n source?: string;\n /** The App's ceiling for this resource. A declared resource reads; `'write'`\n is the one that is written. With role grants, roles grant within it. An\n App serves nothing it does not declare. */\n access?: 'write';\n /** In a tenant App (`auth.rows: 'tenant'`), whose rows these are: one\n tenant's, every tenant's read-only, or the platform realm's. Required on\n every resource there; refused anywhere else. */\n owner?: ResourceOwner;\n /** In a tenant App, what a clone of a tenant into a sandbox copies of this\n tenant-owned resource: `config` rows always, `sample` rows only when the\n clone asks for samples. Unmarked, nothing (tenantClone.ts). */\n clone?: ResourceClone;\n /** `false`: on an App with `live: true`, a write to this resource's table is not announced and no page follows\n it (a hot or log-like table); its pages refresh on focus and after their own saves. Absent: the App's choice. */\n live?: false;\n /** Whether the App deletes this resource's records. Opt-in: absent, the\n server refuses a delete and no Delete is drawn (appDeletable). */\n delete?: true;\n /** Whether the resource's records carry a Favorites star. Opt-in. */\n favorite?: true;\n /** A settings record: the resource's one row, opened from the reader's\n settings (the account menu) under these words. */\n settings?: string;\n /** What one record is called and several: the New button, a split's detail heading, a page's\n label and a list's when they say none. Absent, the table's own name. */\n noun?: AppNoun;\n /** The column ceiling within `access`. Complete primary keys stay readable. */\n fields?: AppFieldAccess;\n /** Columns no reader of the App is ever served, signed in or not: cut from the catalog\n the server answers with, so every read path refuses them as columns that are not\n there. Intersects every role's field grant; never the primary key. */\n hidden?: string[];\n /** Soft keys: column → the resource it names and the column it is looked up by (absent: the target's\n primary key). Joined like a foreign key within one connection (declaredReferences.ts), read in two steps into\n another (appStitchedReads.ts); never authority for a write. */\n references?: Record<string, { to: string; column?: string }>;\n /** Column → how its values dress, everywhere they appear. */\n display?: Record<string, AppDisplayRule>;\n /** Column → the kind of value a TEXT column holds, where its declared type cannot say: `{ due_on: 'date' }`\n for ISO dates, so windows, timelines, calendars, date presets and date filters take it. SQLite only; the\n served catalog says the column is a date, and the database still holds text (declaredDates). */\n types?: Record<string, DeclaredDateKind>;\n /** Column → the App's own input component, drawn in its edit and New forms in place of the platform's control. */\n inputs?: Record<string, import('@mistriwork/engine/shared/appComponents.js').ComponentDecl>;\n /** Resource invariants, whichever form, action or command submits the write.\n Runtime activation requires authoritative writer enforcement. */\n validation?: FormRule[];\n /**\n * Under a row policy, the database triggers on this resource's table the App accepts: a command's write that fires\n * one runs, and each row its keyed effects reach is proven inside the person's rows before and after the statement\n * (docs/trigger-effects.md). The effects are read from the trigger, never declared; authority.lock records each\n * named trigger's definition, and a changed one is refused until the lock is approved again. A trigger that only\n * validates needs no name; one that is not named and writes stays refused.\n * @preview\n */\n triggers?: string[];\n /** Column → how long its text may be and what form it takes (textRules.ts): judged as the form is typed and on\n every write, over what the database already says (`ColumnMeta.text`). */\n text?: Record<string, import('@mistriwork/engine/shared/textRules.js').TextRule>;\n /** JSON column → the resource whose rows define its fields (definedFields.ts): drawn, edited, filtered and judged as fields. */\n defined?: Record<string, import('@mistriwork/engine/shared/definedFields.js').DefinedBinding>;\n /** Flows whose steps are rows (appFlows.ts): the tables they are kept in, and what their steps may do to a record. */\n flows?: import('./appFlows.js').FlowBinding;\n /** Column or label path → the App's caption for it, wherever the column is shown. */\n labels?: Record<string, string>;\n /** The path that names one record. Unlike a caption, it may follow a reference.\n `false` is an internal restricted-catalog sentinel; authors can only write a string. */\n recordLabel?: string | false;\n /**\n * The curated \"New\" form: which columns to ask for (the rest are left to\n * the database or to `defaults`), and values filled in before anyone\n * types. Absent means the form the schema writes — every column.\n */\n form?: AppForm;\n /**\n * Buttons on this resource's record page: an update the App has decided\n * in advance (\"Mark working\" sets `status` to `working`), or a server\n * command. Shown only when `when` matches the record, confirmed first\n * when `confirm` says so. A fixed update goes through the same route,\n * gate and row lock as any edit, so it is scoped like one.\n * Keyed by id where authored and served in that order, each carrying its\n * `id`: a header, a queue and a role name an action by it. Absent when the\n * resource declares none.\n */\n actions?: AppAction[];\n /** The rows are files (appFileRules): the bytes, the name, and where an upload's type and size go. */\n file?: AppFileDef;\n /** Rows from a CSV file (appImport.ts): new records through the ordinary insert, or one command per row. */\n import?: import('./appImportDeclaration.js').AppImportDef;\n /** The records carry tags, rows of `to` linked through `through` (appTagRules). */\n tags?: AppTagsDef;\n /** Search through a search provider's connection: the columns the index holds. */\n search?: AppSearchDef;\n /** The record's states and verbs (appMachine.ts): its column moves only along the graph, on every door, and its\n verbs lead the resource's `actions`. */\n machine?: import('./appMachine.js').AppMachineDef;\n /** Timers at an instant a column holds (appAt.ts): a command run through the jobs once its time has come and `when` holds. */\n at?: import('./appAt.js').AppAtTimer[];\n /** The page this resource's records open through when two pages share its\n base. The loader also orders that page first among its base's pages\n (orderAppPages), for the readers that take the first. */\n page?: string;\n /** The list view its records are addressed through (appAddress.ts). */\n view?: string;\n /** The natural key column its records are addressed by, instead of the primary key. */\n address?: string;\n /** A slug after a numeric id on public pages. */\n slug?: true;\n /** What the support write trail keeps of these rows: which columns are\n recorded as a keyed hash rather than their value. */\n audit?: AppAuditDef;\n /** Numbers about the rows that point at each record, read like columns\n (appRollups.ts): `line_total` → the sum of its lines' amounts. */\n rollups?: Record<string, AppRollup>;\n /** Virtual columns, each a typed expression over the record's own columns, read like columns and never\n written (computed.ts, appComputed.ts): `is_overdue` → `due_on < today and not completed`. */\n computed?: Record<string, AppComputed>;\n /** Which writes tell which people, kept in each person's inbox (appNotify.ts). */\n notify?: import('./appNotify.js').AppNotifyRule[];\n /** Named queries over the resource's rows (appSegments.ts): a tab, a metric, a related list or a rollup names\n one, and the query language reads one as a word (`overdue and mine`). */\n segments?: Record<string, AppSegment>;\n /** The parent balances a row of this resource uses up (appConsumes.ts): a write of the row moves them in its\n own transaction, and refuses one that would take a balance below zero. */\n consumes?: AppConsumes[];\n /** The parent balances a row of this resource adds to (a receipt's line, a return): `consumes` the other way round. */\n restocks?: AppConsumes[];\n}\n\n/** A segment as authored: its conditions in the query language, and what a tab over it is called. */\nexport interface AppSegment {\n query: string;\n /** Its caption: the authored `label`, else its key said as words (`on_bench` → \"On bench\"). */\n label: string;\n}\n\n/**\n * One `consumes` rule, on the line that uses the stock: `column` is one reference of the line and a number\n * column of the record it names (`part_id.on_hand`); `by` a number column of the line, or one per line where\n * absent. `message` is what a refused write says, its `{placeholders}` read from the parent as it stands.\n */\nexport interface AppConsumes {\n /** The line's reference column (`part_id`). */\n reference: string;\n /** The parent's balance column (`on_hand`). */\n balance: string;\n by?: string;\n message?: string;\n /** false: the balance may go below zero (a backorder). */\n floor?: false;\n}\n\n/**\n * A rollup as authored: `count('task', { whose: 'account_id', query: 'status = open' })`,\n * `sum('order_line', 'amount')`. `from` is a resource whose rows point at\n * this one through one reference (`whose`, needed only when there are two),\n * or through two (`whose: 'task_id.project_id'`, appRollups.ts);\n * `query` narrows which of them count, over their own columns.\n */\nexport interface AppRollup {\n fn: import('@mistriwork/engine/shared/rollups.js').RollupFn;\n from: string;\n column?: string;\n whose?: string;\n query?: string;\n /** A `latest`'s order: the column of `from` whose greatest value is the latest row; its key when absent. */\n by?: string;\n}\n\n/** A computed column as authored: what it is, and its expression in the query language's words. */\nexport interface AppComputed {\n type: import('@mistriwork/engine/shared/computed.js').ComputedType;\n value: string;\n}\n\n/** Existing-record save preference; grants no write capability. */\nexport interface AppEditing {\n save: 'explicit' | 'blur';\n}\n\n/** Normalized chrome and formatting. The server-only branding descriptor is\n separated from its static fallback theme during parsing. */\n/** `ui.timezone` read per reader: a text column of a resource, `resource.column`, and the App's own zone. */\nexport interface AppReaderZone {\n column: string;\n default?: string;\n}\n\n/**\n * An App's own logo picture (`ui.logo: { src, dark, mark }`): files in its directory, and, once the load has read\n * them, where each is served (`href`, `darkHref`, `markHref`; server/app/appBrand.ts).\n */\nexport interface AppLogoImage {\n src: string;\n dark?: string;\n mark?: string;\n href?: string;\n darkHref?: string;\n markHref?: string;\n}\n\nexport interface AppUi {\n /** Explicit authored choices; omission never infers an App menu. */\n /* Served said: every caption is the reader's words by now (server/app/appMessages.ts). */\n appSwitcher?: SaidCaptions<import('../../schema/app.js').AppSwitcher>;\n /** The App's footer, drawn under every room: a line of small print and a\n row of links. The landing wears it too. */\n footer?: LandingFooter;\n /** Decorative brand mark drawn beside the App name: an icon, or the App's own picture. The name remains its\n accessible label. */\n logo?: AppIconName | AppLogoImage;\n theme?: AppTheme;\n /** Internal only: authored as ui.theme: fromRecord(...), never as ui.branding.\n Bind against the logical catalog before reading an authorized row. */\n branding?: RecordBrandingSpec;\n /** Registered themes, offered in the reader's theme picker. */\n customThemes?: ThemeDefinition[];\n /** false hides the Export menu everywhere the app curates, and the server refuses the file as well, over any role's export grant. Default true. */\n export?: boolean;\n /** true makes the App an installable PWA (app/appPwa.ts): its pages carry its manifest and worker registration, and it serves its worker, icons and offline page. Preview: the shape may change in any 1.x release. Absent or false, it serves none of those. */\n pwa?: boolean;\n /** App shell visibility and spacing; omitted settings keep the existing UI. */\n shell?: AppShell;\n /** Omission keeps explicit Save. Workbench always uses explicit review/apply. */\n editing?: AppEditing;\n /** BCP 47 tag every figure the app writes is in — numbers, money, dates, counts (\"en-AU\",\n \"de-DE\") — and the language its captions are said in (messages/<locale>.ts). Absent means\n the reader's browser decides the figures and the source catalog the words. Authored as\n `{ column, default }`, this is `default`, and served it is the reader's own (appLocale.ts). */\n locale?: string;\n /** `ui.locale: { column: 'person.locale' }`: the text column each reader's own tag is read from\n (`resource.column`, their first visible row). Never served. */\n localeColumn?: string;\n /** `ui.messages: fromRecord(...)`: a tenant's own words over the App's catalogs. Never served. */\n messages?: import('./branding.js').RecordMessagesSpec;\n /** IANA zone the app's \"today\" is reckoned in (\"Australia/Sydney\"). Date\n phrases — last 30 days, this month — are windows on this calendar.\n Absent means the server's own clock. A TIMESTAMP or DATETIME that carries\n no zone holds the wall clock in this zone (UTC when the App names none) — whatever\n writes it: an action's `now`, a machine's `since`, a `Date` a command\n writes, a New form's `now`. A column with a zone (timestamptz) holds the instant.\n `{ column: 'desk_setting.timezone', default }` reads each reader's days in the\n zone their first visible row of that column names (a tenant's, an account's);\n writes keep `default`, which stands in where the row names none. Read it\n through `appZone` (appUi.ts), never directly. */\n timezone?: string | AppReaderZone;\n /** ISO 4217 default for every money rule that names none. Defaults to USD. */\n currency?: string;\n /** The origins the page opens beside its own for the App's custom components. Opening egress is authority, so\n authority.lock records each. */\n csp?: {\n /** Origins they may send requests to (`https://api.example.com`): the page's `connect-src`. */\n connect?: string[];\n /** Origins they may load pictures from: the page's `img-src`. */\n images?: string[];\n };\n}\n\nexport interface AppDef {\n /** The words a value component's `label` gave its column's vocabulary, by \"resource.column\" (server/app/valueLabels.ts):\n learned against the catalog the App is served on, never authored, and shared by reference with the definition\n said in a reader's other words so one learning serves every reader. */\n valueLabels?: Record<string, Record<string, string>>;\n /** authority.lock's approved trigger digests, by \"resource.trigger\" (server/commands/triggerLock.ts): read with the\n lock when the App loads, never authored; absent where the App keeps no lock. */\n approvedTriggers?: Record<string, string>;\n /** Private control declarations; browser payloads serve decisions separately. */\n flags?: import('./appFlags.js').FlagCatalog['flags'];\n /** Named audiences that flag rules and experiment eligibility reach by name (appFlags.ts AudienceDefinition). */\n audiences?: import('./appFlags.js').FlagCatalog['audiences'];\n experiments?: Readonly<Record<string, import('./appExperiments.js').ExperimentDeclaration>>;\n /** What the App's behavior analytics collect, emit and funnel; off when absent (appAnalytics.ts). */\n analytics?: import('./appAnalytics.js').AnalyticsDeclaration;\n /** The in-App assistant, offered to signed-in people under their own grants (appAssistant.ts, docs/assistant.md). */\n assistant?: import('./appAssistant.js').AppAssistant;\n /** How long the runtime keeps its history, command and attempt rows; absent windows are their defaults (appRetention.ts). */\n retention?: import('./appRetention.js').AppRetention;\n /** Explicit integration exposure; checked against the logical catalog before loading. */\n api?: import('./appApi.js').AppApiDef;\n /**\n * What the App's commands keep, unless a command says otherwise: `none`,\n * `receipts` (a durable receipt, so a retried request is answered once) or\n * `audit` (the receipt and every change a command made). Whatever is\n * declared, a command reached by a door that retries — the App API, MCP, a\n * job — keeps at least receipts. Absent: none for the browser's own\n * requests.\n */\n track?: 'none' | 'receipts' | 'audit';\n /**\n * Whether the App's open pages follow other people's changes (live sync):\n * on only when it says so. The operator's `--no-live` turns it off whatever\n * the App says. A resource leaves it with its own `live: false`.\n */\n live?: true;\n id: string;\n name: string;\n /** What a phone's bar and an installed icon say where `name` does not fit; 12 characters or fewer. */\n shortName?: string;\n /** Connection this app is for, by id or name. Absent means every connection. */\n connection?: string;\n /** Every resource the App declares, by logical name. A physical binding\n is optional: an undeclared binding is inferred from a unique name. */\n resources?: Record<string, ResourceDef>;\n /** Read-only resources over tables that share a shape, each read in one order and one page (appUnions.ts). */\n unions?: readonly import('./appUnions.js').AppUnion[];\n /** Named server-side bindings; connection is normalized to the default binding. */\n sources?: Record<string, AppSourceBinding>;\n /**\n * The App's views by key: the rooms a signed-in visitor opens — every\n * list, dashboard and signed-in form view nav references, every one a\n * home rule names (the one room of an App without nav), and every list\n * view a dashboard embeds, which its \"open →\" opens though no menu lists\n * it — and every public form, served at its own address.\n */\n views: Record<string, AppListView | AppDashboardView | AppFormView>;\n /** The menu as authored: references to `views`, and groups of them. Empty\n for an App without nav, which draws no menu. */\n nav: AppNavNode[];\n /** Where each visitor starts, as authored: rules read in order, the first\n that matches wins, and the last matches everyone. The server evaluates\n them per request (appHome.ts resolveHome); a view id is one rule. */\n home: AppHomeRule[];\n /** Chrome and formatting, as authored under `ui`. Always present; empty\n when the App sets none of it. */\n ui: AppUi;\n /**\n * Sign-in for this app, through Better Auth, and its role grants. Absent\n * means the server's own trust model (loopback, or a proxy header).\n * Present, every request to the app needs a session, and every write is\n * journaled with who made it.\n */\n auth?: AppAuth;\n /** The front page, under its view key: content only, no data, served\n signed out. Requires auth. */\n landing?: { view: string; content: AppLanding };\n /** What each tenant may change about how the App looks to its people, and who may change it (tenantBranding.ts). */\n branding?: import('./tenantBranding.js').BrandingDeclaration;\n}\n\n/** One home rule: `signedOut` matches a visitor with no session, `roles` a\n signed-in one holding any of them, and a rule with neither everyone. */\nexport interface AppHomeRule {\n view: string;\n signedOut?: true;\n roles?: string[];\n realm?: AppRealm;\n}\n\n/* The enumerations the parsers check against, exported so the JSON schema is\n pinned to them (test/schema.test.ts) instead of repeating them by hand. */\nexport const DASH_KINDS = [\n 'metric',\n 'breakdown',\n 'rows',\n 'chart',\n 'note',\n 'matrix',\n 'funnel',\n 'board',\n 'firstRun',\n 'custom',\n 'activity',\n 'find',\n 'branding',\n 'integrations',\n] as const;\n/** How many changes an activity section draws before \"Show more\", at most, and how many one read answers. */\nexport const ACTIVITY_MAX = 50;\n/** How many steps a first run asks: a start, not a manual. */\nexport const FIRST_RUN_MAX_STEPS = 6;\nexport const OPEN_MODES = ['page', 'drawer', 'modal'] as const;\nexport { FILTER_BARS, type FilterBar } from '@mistriwork/engine/shared/listPresentations.js';\nexport const SHELL_WIDTHS = ['full', 'wide', 'narrow'] as const;\nexport const SHELL_HEADER_WIDTHS = ['full', 'content'] as const;\nexport const SHELL_GUTTERS = ['none', 'compact', 'normal', 'comfortable'] as const;\n/** A dashboard's charts draw these as well; a record page's chart keeps to CHART_SHAPES. */\nexport const DASH_CHART_SHAPES = [...CHART_SHAPES, 'scatter', 'heatmap', 'waterfall', 'treemap'] as const;\nexport const BETTER = ['up', 'down'] as const;\n\n/** The windows offered when a dashboard declares none of its own. */\nexport const DEFAULT_WINDOWS = ['last 30 days', 'last 90 days', 'last 12 months'];\n\n/**\n * A list view as served: the table re-addressed to its resolved id, the\n * label always filled in, and a view whose table or columns this connection\n * does not have carries its reason instead of vanishing.\n */\nexport interface ResolvedListView extends Omit<AppListView, 'label'> {\n label: string;\n error?: string;\n}\n\n/** The source whose resources stay bare: the one that says `default: true`, if one does. */\nexport function defaultSourceOf(app: Pick<AppDef, 'sources'>): string | undefined {\n for (const [name, source] of Object.entries(app.sources ?? {})) if (source.default) return name;\n return undefined;\n}\n\n/**\n * The App's own source: the connection it is served under, where its\n * accounts, command journal and editing history live. Its default source,\n * or, when none is the default, the first of its own sources. Separate from\n * naming, so an App whose sources all keep `source.name` still has a home.\n * Undefined for an App without named sources.\n */\nexport function homeSourceOf(app: Pick<AppDef, 'sources'>): string | undefined {\n return defaultSourceOf(app) ?? ownSourceNames(app)[0];\n}\n\n/** The platform-only sources (`realm: 'platform'`), in declaration order: none for most Apps. */\nexport function platformSourceNames(app: Pick<AppDef, 'sources'>): string[] {\n return Object.entries(app.sources ?? {})\n .filter(([, source]) => source.realm === 'platform')\n .map(([name]) => name);\n}\n\n/** The App's own sources: every source but its platform-only ones. What has\n * one catalog counts these — the API's, and a tenant App's one source — since\n * a platform-only source is read by the platform realm's reads alone. */\nexport function ownSourceNames(app: Pick<AppDef, 'sources'>): string[] {\n return Object.keys(app.sources ?? {}).filter((name) => app.sources![name].realm !== 'platform');\n}\n\n/** A source's table under its logical name: bare in the default source, `source.name` in any other. */\nexport function qualifyResource(defaultSource: string | undefined, source: string, local: string): string {\n return source === defaultSource ? local : `${source}.${local}`;\n}\n\n/** The logical name of a source's table in this App, which a command, a catalog and a grant all speak. */\nexport function logicalResource(app: Pick<AppDef, 'sources'>, source: string, local: string): string {\n return qualifyResource(defaultSourceOf(app), source, local);\n}\n\n/**\n * One catalog's declared physical bindings, local name → physical table id:\n * those of `source`, or, without one, those of an App without named sources.\n * An undeclared binding is the catalog's to infer from a unique name.\n */\nexport function physicalBindings(app: Pick<AppDef, 'resources'>, source?: string): Record<string, string> {\n const bindings: Record<string, string> = {};\n for (const [name, resource] of Object.entries(app.resources ?? {})) {\n if (resource.physical === undefined || resource.source !== source) continue;\n bindings[name.slice(name.indexOf('.') + 1)] = resource.physical;\n }\n return bindings;\n}\n\n/** The source a reference's target belongs to: its resource block's, as parsing recorded it. */\nconst targetSource = (app: Pick<AppDef, 'resources'>, to: string): string | undefined => app.resources?.[to]?.source;\n\n/** The App's declared references within one source (the whole App, without sources), in that source's local names. */\nexport function declaredReferences(\n app: Pick<AppDef, 'resources'>,\n source?: string,\n): import('@mistriwork/engine/shared/declaredReferences.js').DeclaredReference[] {\n const local = (name: string) => name.slice(name.indexOf('.') + 1);\n return Object.entries(app.resources ?? {}).flatMap(([name, resource]) =>\n resource.source !== source\n ? []\n : Object.entries(resource.references ?? {})\n .filter(([, reference]) => targetSource(app, reference.to) === source)\n .map(([column, reference]) => ({\n table: local(name),\n column,\n to: local(reference.to),\n ...(reference.column ? { toColumn: reference.column } : {}),\n })),\n );\n}\n\n/**\n * The App's declared references from one source to another, in the App's\n * resource names (`stock_move` → `catalog.product`). Joined in SQL where the\n * two sources share a connection, read in two steps where they do not\n * (appSourceCatalog.ts).\n */\nexport function crossSourceReferences(\n app: Pick<AppDef, 'resources'>,\n): import('@mistriwork/engine/shared/declaredReferences.js').DeclaredReference[] {\n return Object.entries(app.resources ?? {}).flatMap(([name, resource]) =>\n Object.entries(resource.references ?? {})\n .filter(([, reference]) => targetSource(app, reference.to) !== resource.source)\n .map(([column, reference]) => ({\n table: name,\n column,\n to: reference.to,\n ...(reference.column ? { toColumn: reference.column } : {}),\n })),\n );\n}\n\n/** What a declared type says a TEXT column holds: ISO dates (`2026-09-24`) or date-times (`2026-09-24 14:30:00`). */\ntype DeclaredDateKind = 'date' | 'datetime';\n/** A text column an App declares a date, in its source's local names. */\nexport interface DeclaredDate {\n table: string;\n column: string;\n as: DeclaredDateKind;\n}\n\n/** The App's `types` declarations within one source (`undefined`: the App's only source), in local names. */\nexport function declaredDates(app: Pick<AppDef, 'resources'>, source?: string): DeclaredDate[] {\n const local = (name: string) => name.slice(name.indexOf('.') + 1);\n return Object.entries(app.resources ?? {}).flatMap(([name, resource]) =>\n resource.source !== source ? [] : Object.entries(resource.types ?? {}).map(([column, as]) => ({ table: local(name), column, as })),\n );\n}\n\n/** The App grants by role: sign-in with default-deny role grants within the resource ceilings. */\nexport function grantsByRole(app: Pick<AppDef, 'auth'>): boolean {\n return app.auth?.roles !== undefined;\n}\n\n/** Rows scoped by a server-defined row policy (`auth.rows: 'policy'`). */\nexport function isRowPolicyApp(app: Pick<AppDef, 'auth'>): boolean {\n return app.auth?.rows === 'policy';\n}\n\n/** Rows scoped by tenant membership (`auth.rows: 'tenant'`). */\nexport function isTenantApp(app: Pick<AppDef, 'auth'>): boolean {\n return app.auth?.rows === 'tenant';\n}\n\nexport { FILE_MAX_BYTES_CEILING, FILE_MAX_BYTES_DEFAULT } from '@mistriwork/engine/shared/fileBounds.js';\n\n/**\n * The resource ceilings by logical name, in declaration order: `read` is\n * every declared resource (a resource reads unless it says otherwise),\n * `write` those whose access is 'write',\n * `fields` the column ceilings. What the resources say, unchecked; the\n * resolver re-checks it (checkedGrants) before any grant is judged.\n */\nexport function resourceCeiling(app: Pick<AppDef, 'resources'>): {\n read: string[];\n write: string[];\n fields?: Record<string, AppFieldAccess>;\n} {\n const declared = Object.entries(app.resources ?? {});\n const fielded = declared.filter(([, resource]) => resource.fields);\n return {\n read: declared.map(([name]) => name),\n write: declared.filter(([, resource]) => resource.access === 'write').map(([name]) => name),\n ...(fielded.length ? { fields: Object.fromEntries(fielded.map(([name, resource]) => [name, resource.fields!])) } : {}),\n };\n}\n\n/** Every resource's invariants by logical name, or undefined when none declares any. */\nexport function resourceValidation(app: Pick<AppDef, 'resources'>): Record<string, FormRule[]> | undefined {\n const declared = Object.entries(app.resources ?? {}).filter(([, resource]) => resource.validation);\n return declared.length ? Object.fromEntries(declared.map(([name, resource]) => [name, resource.validation!])) : undefined;\n}\n\n/**\n * Why a key the types declare ahead of its feature is refused. Each such\n * key is parsed and refused with one of these, so an author learns the\n * feature is not built yet rather than that the key is a typo. The keys\n * are the work that turns each one on; the texts never name it.\n */\nexport const LATER = {\n T8: 'is not supported yet: it needs support sessions',\n} as const;\n\n/** The keys each authored section kind takes. Anything else is refused by\n name. Exported so the JSON schema's per-kind objects are pinned to it. A\n board's card is `primary` plus `lines` (the one card vocabulary); it is\n read into the section's `show`. A board takes no `width`: its lanes\n always have the dashboard's whole width. A `segment` is read before the\n section is (appSegments.ts unfoldSegments), as its word in `query`. */\nexport const SECTION_KEYS: Record<AppDashSection['kind'], readonly string[]> = {\n metric: [\n 'kind',\n 'area',\n 'title',\n 'icon',\n 'from',\n 'query',\n 'segment',\n 'measure',\n 'compare',\n 'better',\n 'spark',\n 'over',\n 'windows',\n 'width',\n 'target',\n 'to',\n 'format',\n 'unit',\n 'figure',\n ],\n breakdown: [\n 'kind',\n 'area',\n 'title',\n 'icon',\n 'from',\n 'query',\n 'segment',\n 'measure',\n 'by',\n 'shape',\n 'notes',\n 'over',\n 'windows',\n 'limit',\n 'width',\n 'format',\n 'unit',\n 'target',\n 'to',\n 'empty',\n ],\n chart: [\n 'kind',\n 'area',\n 'title',\n 'icon',\n 'from',\n 'query',\n 'segment',\n 'measure',\n 'x',\n 'by',\n 'shape',\n 'curve',\n 'total',\n 'over',\n 'windows',\n 'limit',\n 'width',\n 'height',\n 'target',\n 'format',\n 'unit',\n 'to',\n 'empty',\n ],\n funnel: ['kind', 'area', 'title', 'icon', 'from', 'query', 'segment', 'stages', 'over', 'windows', 'width', 'to', 'empty'],\n matrix: [\n 'kind',\n 'area',\n 'title',\n 'icon',\n 'from',\n 'query',\n 'segment',\n 'measure',\n 'by',\n 'over',\n 'windows',\n 'width',\n 'format',\n 'unit',\n 'to',\n 'empty',\n ],\n rows: [\n 'kind',\n 'area',\n 'title',\n 'icon',\n 'from',\n 'query',\n 'segment',\n 'show',\n 'sort',\n 'limit',\n 'over',\n 'windows',\n 'width',\n 'sizing',\n 'empty',\n ],\n board: ['kind', 'area', 'title', 'icon', 'from', 'query', 'segment', 'by', 'lanes', 'primary', 'lines', 'sort', 'limit', 'over', 'empty'],\n note: ['kind', 'area', 'title', 'text', 'icon', 'action', 'width', 'variant', 'eyebrow', 'image'],\n firstRun: ['kind', 'area', 'title', 'description', 'steps'],\n custom: ['kind', 'area', 'title', 'width', 'component', 'rows', 'figures', 'props'],\n activity: ['kind', 'area', 'title', 'icon', 'resources', 'limit', 'width'],\n find: ['kind', 'area', 'title', 'icon', 'resources', 'placeholder', 'width'],\n branding: ['kind', 'area', 'title', 'icon', 'width'],\n integrations: ['kind', 'area', 'title', 'icon', 'width'],\n};\n\n/** The keys a dashboard's embed of a list view takes (`{ kind: 'view', view }`). */\nexport const VIEW_SECTION_KEYS = [\n 'kind',\n 'view',\n 'area',\n 'title',\n 'icon',\n 'width',\n 'tab',\n 'limit',\n 'over',\n 'windows',\n 'interactive',\n 'empty',\n] as const;\n","import type { CommandFieldPresentation } from '@mistriwork/engine/schema/model.js';\nimport type {\n AppCommandChoice,\n AppCommandField,\n AppCommandReference,\n AppFormCommandReference,\n CommandFieldDefault,\n CommandRichTextMentions,\n} from '../../schema/command.js';\nimport { columnKind, tableNamed, type DatabaseSchema, type TableMeta } from '@mistriwork/engine/shared/schema.js';\nimport { ANYONE, parseQuery, quoteValue } from '@mistriwork/engine/shared/query.js';\nimport { maskQuoted } from '@mistriwork/engine/shared/maskQuoted.js';\nimport { parseViewQuery } from '@mistriwork/engine/shared/view.js';\nimport { unknownKey } from '@mistriwork/engine/shared/suggest.js';\nimport { normalizeAppFlagGate } from './appFlagGates.js';\nimport type { FlagCatalog } from './appFlags.js';\nimport { RICH_TEXT_MOST } from '@mistriwork/engine/shared/richText.js';\nimport { mentionLabelColumns } from '@mistriwork/engine/shared/mentionLabel.js';\nimport { commandFieldsDeep, isIsoDate } from '@mistriwork/engine/shared/commandWire.js';\nimport type { DraftRule } from './commandDraft.js';\nexport { BULK_LIMIT, commandFieldsDeep, commandKeyText, isIsoDate } from '@mistriwork/engine/shared/commandWire.js';\n\nexport type {\n Action,\n AppAction,\n AppUpdateAction,\n AppCommandAction,\n AppCommandChoice,\n AppCommandField,\n AppCommandGroup,\n AppCommandLines,\n AppCommandReference,\n AppFormCommandReference,\n AppFlagGate,\n CommandAction,\n CommandField,\n CommandInput,\n CommandInputOf,\n CommandInitialOf,\n} from '../../schema/command.js';\n\n/** A served reference without `targets` names one record. */\nexport const commandTargets = (reference: { targets?: 'one' | 'many' }): 'one' | 'many' => reference.targets ?? 'one';\n\nconst reserved = new Set([...Object.getOwnPropertyNames(Object.prototype), 'prototype']);\nconst identifier = /^[A-Za-z_][A-Za-z0-9_]*$/;\nconst fail = (where: string, reason: string): never => {\n throw new Error(`${where}: ${reason}.`);\n};\n\nfunction record(raw: unknown, keys: readonly string[], where: string): Record<string, unknown> {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw) || ![null, Object.prototype].includes(Object.getPrototypeOf(raw)))\n return fail(where, 'expected a plain data object');\n const result: Record<string, unknown> = Object.create(null);\n for (const key of Reflect.ownKeys(raw)) {\n if (typeof key !== 'string') return fail(where, 'unknown command metadata property');\n if (!keys.includes(key)) throw new Error(`${where} ${unknownKey(key, keys)}.`);\n const descriptor = Object.getOwnPropertyDescriptor(raw, key)!;\n if (!descriptor.enumerable || !('value' in descriptor)) return fail(where, 'expected enumerable data properties');\n result[key] = descriptor.value;\n }\n return result;\n}\n\nfunction text(raw: unknown, where: string, limit = 500): string {\n if (typeof raw !== 'string' || !raw.trim() || raw.length > limit)\n return fail(where, `expected nonempty text of at most ${limit} characters`);\n return raw.trim();\n}\n\n/* A mention's label: a column, or a template of the people's own columns ('{first_name} {last_name}', mentionLabel.ts). */\nfunction mentionLabel(raw: unknown, where: string): string {\n if (typeof raw !== 'string' || !raw.includes('{')) return name(raw, where);\n const read = mentionLabelColumns(raw);\n if ('problem' in read) return fail(where, `label ${read.problem}`);\n for (const column of read.columns) name(column, `${where}, label \"${raw}\"`);\n return raw;\n}\n\nfunction name(raw: unknown, where: string, qualified = false): string {\n if (typeof raw !== 'string' || raw.length > 128) return fail(where, 'expected a logical identifier');\n const parts = raw.split('.');\n if (parts.length > (qualified ? 2 : 1) || parts.some((part) => !identifier.test(part) || reserved.has(part))) {\n return fail(where, 'expected a non-reserved logical identifier');\n }\n return raw;\n}\n\n/**\n * The caps a command's input declaration and its values live under. A list\n * of fields (the command's own, or a group's) holds at most `listFields`; the\n * whole declaration, groups and their members at every depth, at most\n * `totalFields`; a group may hold a group, and no deeper (`depth`). A\n * repeating group admits at most `repeatRows` rows, and on a public form at\n * most `publicRepeatRows`; a public submission's input is at most\n * `publicInputBytes` of JSON, inside the envelope bound every command has.\n */\nexport const COMMAND_INPUT_LIMITS = Object.freeze({\n listFields: 32,\n totalFields: 128,\n depth: 2,\n repeatRows: 50,\n publicRepeatRows: 10,\n choiceOptions: 100,\n publicInputBytes: 32_768,\n});\n\n/* A moment, as ISO 8601 says one: a date, a time to the minute or finer, and a zone — `Z` or an offset. The\n separator may be the `T` or a space, as Postgres writes a timestamptz (`2026-10-12 19:16:00-04`). */\nconst INSTANT = /^(\\d{4}-\\d{2}-\\d{2})[T ]([01]\\d|2[0-3]):([0-5]\\d)(?::([0-5]\\d)(?:\\.(\\d{1,9}))?)?(Z|[+-](?:0\\d|1[0-4])(?::?[0-5]\\d)?)$/;\n\n/**\n * A moment as one spelling: the UTC instant `YYYY-MM-DDTHH:MM:SS.sssZ` it names, or undefined. A value with no zone\n * is not one — the same wall time is a different moment in every zone — and a `Date` is read as the moment it holds.\n * Beyond milliseconds a fraction is dropped, as a JavaScript date keeps it.\n */\nexport function isoInstant(value: unknown): string | undefined {\n let time: number;\n if (value instanceof Date) time = Date.prototype.getTime.call(value);\n else {\n if (typeof value !== 'string' || value.length > 40) return undefined;\n const match = INSTANT.exec(value);\n if (!match || !isIsoDate(match[1])) return undefined;\n const [, date, hour, minute, seconds = '00', fraction = '', zone] = match;\n const digits = zone.replace(':', '');\n const offset = zone === 'Z' ? 0 : (zone[0] === '-' ? -1 : 1) * (Number(digits.slice(1, 3)) * 60 + Number(digits.slice(3, 5) || 0));\n time = Date.parse(`${date}T${hour}:${minute}:${seconds}.${fraction.padEnd(3, '0').slice(0, 3)}Z`) - offset * 60_000;\n }\n if (!Number.isFinite(time)) return undefined;\n const at = new Date(time);\n return at.getUTCFullYear() >= 0 && at.getUTCFullYear() <= 9999 ? at.toISOString() : undefined;\n}\n\n/** Dense, plain list items, each a data property. */\nfunction items(raw: unknown, where: string, what: string, max: number, noun: string): unknown[] {\n if (\n !Array.isArray(raw) ||\n Object.getPrototypeOf(raw) !== Array.prototype ||\n raw.length > max ||\n Reflect.ownKeys(raw).length !== raw.length + 1\n )\n return fail(where, `${what} must be a dense list of at most ${max} ${noun}`);\n const result: unknown[] = [];\n for (let index = 0; index < raw.length; index++) {\n const descriptor = Object.getOwnPropertyDescriptor(raw, String(index));\n if (!descriptor?.enumerable || !('value' in descriptor)) return fail(where, `${what} must contain only enumerable data ${noun}`);\n result.push(descriptor.value);\n }\n return result;\n}\n\n/** The names a field's `when` reads — the input's fields and the record's columns — or a throw where it does not parse. */\ntype ConditionNames = (text: string, where: string) => { fields: string[]; record: string[] };\n\ninterface FieldRules {\n depth: number;\n publicForm: boolean;\n count: { fields: number };\n /* The condition compiler's reading of a `when` (commandDraft.ts draftNames), handed in by parseCommandInput\n (commandInputParse.ts): this module is in every page's module graph, the browser never parses a declaration,\n and a static import here put the compiler on every public page's first paint. */\n names: ConditionNames;\n}\n\nfunction choiceOption(raw: unknown, where: string): AppCommandChoice {\n const o = record(raw, ['value', 'label', 'icon', 'help'], where);\n const value = o.value;\n if (!(typeof value === 'string' && value.trim() && value.length <= 120) && !(typeof value === 'number' && Number.isFinite(value))) {\n return fail(where, 'a choice value must be nonempty text of at most 120 characters or a finite number');\n }\n if (o.icon !== undefined && (typeof o.icon !== 'string' || !/^[a-z][a-z0-9-]{0,63}$/.test(o.icon)))\n return fail(where, \"a choice's icon is an App icon's name, such as 'shield-check'\");\n return {\n value: value as string | number,\n label: text(o.label, where, 120),\n ...(o.icon === undefined ? {} : { icon: o.icon as string }),\n ...(o.help === undefined ? {} : { help: text(o.help, where, 200) }),\n };\n}\n\n/** A field's label when it gives none, read from its key: `applicant_email` is \"Applicant email\", `reviewerId` \"Reviewer id\". */\nexport function fieldLabel(key: string): string {\n const words = key\n .replace(/([a-z0-9])([A-Z])/g, '$1 $2')\n .replace(/_+/g, ' ')\n .trim()\n .toLowerCase();\n return words.charAt(0).toUpperCase() + words.slice(1);\n}\n\n/** A declared pattern, matched whole: what the browser and the server both test a value against. */\nexport const fieldPattern = (source: string): RegExp => new RegExp(`^(?:${source})$`);\n\nconst FORMATS = new Set(['email', 'url', 'phone']);\n\n/** A rich text input's most characters of stored HTML, unless it says otherwise: room for a long note with a table in it. */\nconst RICH_TEXT_FIELD_DEFAULT = 65_536;\n\n/* What a field starts as. A literal, today, now and the record's own value\n are the browser's or prepare's to fill in, so only the shape is judged\n here; whether a literal passes the field's own checks is the server's,\n where the command is defined (businessCommands.ts). */\nconst START_WORDS: Record<string, readonly string[]> = { today: ['date'], now: ['datetime'], me: ['text'] };\nfunction fieldStart(raw: unknown, made: AppCommandField, where: string, rules: FieldRules): CommandFieldDefault {\n const at = `${where}: default`;\n if (made.type === 'group') return fail(where, 'a group starts as its fields do; give each of them its default');\n if (raw && typeof raw === 'object' && !Array.isArray(raw)) {\n const start = raw as Record<string, unknown>;\n const word = Object.keys(start).length === 1 ? (start.auto ?? (typeof start.record === 'string' ? 'record' : undefined)) : undefined;\n if (word === 'record' || word === 'me') {\n if (rules.publicForm) return fail(at, 'a public form has no record and no reader to start a field from');\n if (rules.depth) return fail(at, `${word === 'me' ? 'me' : '{ record }'} fills a field of the input itself, not one inside a group`);\n }\n if (word === 'record') return { record: name(start.record, at) };\n if (typeof word === 'string' && START_WORDS[word]?.includes(made.type)) return { auto: word as 'today' | 'now' | 'me' };\n return fail(at, 'is a literal, today (a date field), now (a datetime field), me (a text field) or { record: column }');\n }\n if (typeof raw === 'string' || typeof raw === 'boolean' || (typeof raw === 'number' && Number.isFinite(raw))) return raw;\n return fail(at, 'is a literal, today (a date field), now (a datetime field), me (a text field) or { record: column }');\n}\n\n/* An input asked only while a condition over the draft holds: a bare name is another field of this input, and\n `record.<column>` the target's own. Its words are read here;\n the names it reads are judged beside its siblings (fieldList) and, for the record's, against the target where\n the command is bound (businessCommandAuthorization.ts), rollups and computed columns included. */\nfunction fieldWhen(f: Record<string, unknown>, where: string, rules: FieldRules): string {\n const at = `${where}: when`;\n if (typeof f.when !== 'string' || !f.when.trim() || f.when.length > 1000)\n return fail(at, \"is a condition over the draft, as text: another field's name (trade_in = true) or record.<column>\");\n if (f.type === 'group') return fail(at, \"is a single field's: a group is asked whole\");\n if (rules.depth) return fail(at, 'asks a field of the input itself, not one inside a group');\n let names: { fields: string[]; record: string[] };\n try {\n names = rules.names(f.when.trim(), at);\n } catch (error) {\n return fail(at, (error as Error).message.replace(/^.*?: /, '').replace(/\\.$/, ''));\n }\n if (names.record.length && rules.publicForm) return fail(at, `reads record.${names.record[0]}, and a public form has no record`);\n return f.when.trim();\n}\n\n/* A mask: `0` a digit, `a` a letter, `*` either, anything else a character drawn as written. It is the value's\n pattern too, over what its places hold. */\nconst MASK = /^[^\\p{Cc}]{1,64}$/u;\nexport function maskPattern(mask: string): string {\n return [...mask].map((c) => (c === '0' ? '\\\\d' : c === 'a' ? '[A-Za-z]' : c === '*' ? '[A-Za-z0-9]' : '')).join('');\n}\n/** How many characters a mask's places hold. */\nconst maskPlaces = (mask: string): number => [...mask].filter((c) => c === '0' || c === 'a' || c === '*').length;\n\nconst KEEPS = ['last4', 'hash', 'none', 'handler'] as const;\nconst PRESENTATIONS: Record<string, readonly string[]> = {\n number: ['slider'],\n money: ['slider'],\n boolean: ['buttons'],\n choice: ['cards', 'buttons', 'select', 'radios'],\n choices: ['cards', 'select'],\n};\nconst LIVE_NAME = /^[A-Za-z_][A-Za-z0-9_]{0,31}$/;\nconst COMPONENT_NAME = /^[A-Za-z][A-Za-z0-9_-]{0,47}$/;\n\n/* A month, `YYYY-MM`. */\nexport const isIsoMonth = (value: unknown): value is string => typeof value === 'string' && /^\\d{4}-(0[1-9]|1[0-2])$/.test(value);\n\n/* Choices read from a table: its rows by key, shown by a column. */\nfunction choiceSource(f: Record<string, unknown>, where: string): { from: string; by: string; labelColumn: string } {\n if (f.options !== undefined) return fail(where, 'names its options or a table to read them from (from), not both');\n if (f.labelColumn === undefined)\n return fail(where, `reads its choices from \"${String(f.from)}\" and needs labelColumn: the column each is shown by`);\n return {\n from: name(f.from, `${where}, from`, true),\n by: f.by === undefined ? 'id' : name(f.by, `${where}, by`),\n labelColumn: name(f.labelColumn, `${where}, labelColumn`),\n };\n}\n\n/* `as`: how the field is drawn, one its kind can be drawn as. */\nfunction presentation(f: Record<string, unknown>, where: string): { as?: CommandFieldPresentation } {\n if (f.as === undefined) return {};\n const allowed = PRESENTATIONS[String(f.type)] ?? [];\n if (typeof f.as !== 'string' || !allowed.includes(f.as))\n return fail(\n where,\n allowed.length\n ? `as is how a ${String(f.type)} field is drawn: ${allowed.map((a) => `'${a}'`).join(', ')}`\n : `a ${String(f.type)} field is drawn one way; it takes no as`,\n );\n return { as: f.as as CommandFieldPresentation };\n}\n\n/* `input`: the App's own input component, by its name in `ui.inputs`. A public form's page draws it too: the code is\n served to a visitor like a public page's components, and the value is still the field's kind's to judge. */\nfunction componentName(raw: unknown, where: string): string {\n if (typeof raw !== 'string' || !COMPONENT_NAME.test(raw))\n return fail(where, \"input names one of the App's input components (ui.inputs) by its name\");\n return raw;\n}\n\n/* `live`: lines said under a number as it is typed — expressions over the input's numbers, read where the siblings\n are known (fieldList), never sent. */\nfunction liveLines(raw: unknown, where: string): Record<string, string> {\n const lines = record(raw, Object.keys(raw && typeof raw === 'object' ? raw : {}), `${where}, live`);\n const entries = Object.entries(lines);\n if (!entries.length || entries.length > 4) return fail(where, \"live says one to four lines: { monthly: 'biweekly_gross * 26 / 12' }\");\n for (const [word, expression] of entries) {\n if (!LIVE_NAME.test(word)) return fail(where, `live \"${word}\" is a short word, said after the figure`);\n if (typeof expression !== 'string' || !expression.trim() || expression.length > 500)\n return fail(where, `live \"${word}\" is an expression over this input's numbers, at most 500 characters`);\n }\n return Object.fromEntries(entries.map(([word, expression]) => [word, (expression as string).trim()]));\n}\n\n/** The input names a live line reads: the identifiers in its expression. */\nconst liveNames = (expression: string): string[] =>\n [...new Set(expression.match(/[A-Za-z_][A-Za-z0-9_]*/g) ?? [])].filter((word) => !/^(?:and|or|not|true|false|null)$/.test(word));\n\nfunction field(key: string, raw: unknown, where: string, rules: FieldRules): AppCommandField {\n const made = fieldShape(key, raw, where, rules);\n const start = (raw as { default?: unknown }).default;\n return start === undefined ? made : ({ ...made, default: fieldStart(start, made, where, rules) } as AppCommandField);\n}\n\nfunction fieldShape(key: string, raw: unknown, where: string, rules: FieldRules): AppCommandField {\n if (++rules.count.fields > COMMAND_INPUT_LIMITS.totalFields) {\n return fail(where, `a command declares at most ${COMMAND_INPUT_LIMITS.totalFields} fields, groups and their members included`);\n }\n /* A reference picker's words before 1.0, refused with the ones that replaced them. */\n for (const [old, now] of [\n ['resource', '\"from\": the resource the picker offers'],\n ['value', '\"by\": the key column a pick stores'],\n ] as const) {\n if (raw && typeof raw === 'object' && Object.hasOwn(raw, old)) throw new Error(`${where} has \"${old}\", which is now ${now}.`);\n }\n const f = record(\n raw,\n [\n 'type',\n 'label',\n 'help',\n 'optional',\n 'min',\n 'max',\n 'format',\n 'pattern',\n 'message',\n 'case',\n 'integer',\n 'step',\n 'below',\n 'above',\n 'by',\n 'labelColumn',\n 'query',\n 'options',\n 'fields',\n 'repeat',\n 'keyed',\n 'accept',\n 'description',\n 'mentions',\n 'images',\n 'default',\n 'when',\n 'from',\n 'whose',\n 'show',\n 'copy',\n 'mask',\n 'sensitive',\n 'live',\n 'minor',\n 'currency',\n 'statement',\n 'exclusive',\n 'as',\n 'input',\n ],\n where,\n );\n if (f.keyed !== undefined)\n throw new Error(`${where} has \"keyed\", which is now sensitive: { keep: 'hash' }: one way to keep a value the App must not hold.`);\n if (f.optional !== undefined && typeof f.optional !== 'boolean') return fail(where, 'optional must be true or false');\n // Required unless it says otherwise: a form asks for what its command needs.\n // A group says its words as `description`, as a form's groups do; `help` stays under a single input.\n if (f.type === 'group' && f.help !== undefined)\n throw new Error(`${where} (a group field) ${unknownKey('help', ['type', 'label', 'optional', 'fields', 'repeat', 'description'])}.`);\n if (f.type === 'lines' && f.help !== undefined)\n throw new Error(\n `${where} (a lines field) ${unknownKey('help', ['type', 'label', 'from', 'show', 'copy', 'query', 'min', 'max', 'description'])}.`,\n );\n const base = {\n name: key,\n label: f.label === undefined ? fieldLabel(key) : text(f.label, where, 120),\n ...(f.help === undefined ? {} : { help: text(f.help, where) }),\n ...((f.type === 'group' || f.type === 'lines') && f.description !== undefined ? { help: text(f.description, where) } : {}),\n ...(f.optional === true ? {} : { required: true }),\n ...(f.when === undefined ? {} : { when: fieldWhen(f, where, rules) }),\n ...presentation(f, where),\n ...(f.input === undefined ? {} : { input: componentName(f.input, where) }),\n };\n const common = ['type', 'label', 'help', 'optional', 'default', 'when', 'as', 'input'];\n const extra = Object.keys(f).filter((key) => !common.includes(key));\n const only = (allowed: string[]) => {\n const stray = extra.find((key) => !allowed.includes(key));\n if (stray !== undefined) throw new Error(`${where} (a ${String(f.type)} field) ${unknownKey(stray, [...common, ...allowed])}.`);\n };\n const sibling = (key: 'below' | 'above') => (f[key] === undefined ? {} : { [key]: name(f[key], where) });\n if (f.type === 'text' || f.type === 'textarea') {\n only(['min', 'max', 'format', 'pattern', 'message', 'case', 'mask', 'sensitive']);\n if (f.mask !== undefined) {\n if (f.type !== 'text' || typeof f.mask !== 'string' || !MASK.test(f.mask) || !maskPlaces(f.mask))\n return fail(\n where,\n \"mask is the shape a text's value is typed in: 0 a digit, a a letter, * either ('000-00-0000'), on a text field\",\n );\n if (f.pattern !== undefined || f.format !== undefined)\n return fail(where, 'a mask is its own pattern; give a mask, a pattern or a format');\n }\n let sensitive: { keep: (typeof KEEPS)[number] } | undefined;\n if (f.sensitive !== undefined) {\n const said = record(f.sensitive, ['keep'], `${where}, sensitive`);\n if (f.type !== 'text' || !(KEEPS as readonly unknown[]).includes(said.keep))\n return fail(where, \"sensitive is { keep: 'last4' | 'hash' | 'none' | 'handler' }, on a text field\");\n if (f.default !== undefined) return fail(where, 'a sensitive field is never prefilled; it takes no default');\n if (said.keep === 'handler' && rules.depth)\n return fail(where, \"keep: 'handler' is a field of the input itself, not one inside a group\");\n sensitive = { keep: said.keep as (typeof KEEPS)[number] };\n }\n for (const bound of ['min', 'max'] as const) {\n if (f[bound] !== undefined && (!Number.isSafeInteger(f[bound]) || Number(f[bound]) < 1 || Number(f[bound]) > 16384)) {\n return fail(where, `${bound} must be a number of characters from 1 to 16384`);\n }\n }\n if (f.min !== undefined && f.max !== undefined && Number(f.min) > Number(f.max)) return fail(where, 'min must not exceed max');\n if (f.format !== undefined && (f.type !== 'text' || !FORMATS.has(f.format as string)))\n return fail(where, 'format is \"email\", \"url\" or \"phone\", on a text field');\n if (f.pattern !== undefined) {\n if (typeof f.pattern !== 'string' || !f.pattern || f.pattern.length > 500)\n return fail(where, \"pattern must be a regular expression's source, at most 500 characters\");\n try {\n fieldPattern(f.pattern);\n } catch {\n return fail(where, 'pattern is not a regular expression');\n }\n }\n // A mask is a pattern too, and its sentence is the author's to word (\"Enter the 5 digits of your ZIP\").\n if (f.message !== undefined && f.pattern === undefined && f.format === undefined && f.mask === undefined)\n return fail(where, 'message is what a pattern, format or mask says; give one');\n if (f.case !== undefined && f.case !== 'upper' && f.case !== 'lower') return fail(where, 'case is \"upper\" or \"lower\"');\n return {\n ...base,\n type: f.type,\n ...(f.min === undefined ? {} : { minLength: f.min as number }),\n ...(f.max === undefined ? {} : { maxLength: f.max as number }),\n ...(f.format === undefined ? {} : { format: f.format as 'email' | 'url' | 'phone' }),\n ...(f.pattern === undefined ? {} : { pattern: f.pattern as string }),\n ...(f.message === undefined ? {} : { message: text(f.message, where, 200) }),\n ...(f.case === undefined ? {} : { case: f.case as 'upper' | 'lower' }),\n ...(f.mask === undefined ? {} : { mask: f.mask as string }),\n ...(sensitive ? { sensitive } : {}),\n };\n }\n if (f.type === 'richText') {\n only(['min', 'max', 'mentions', 'images']);\n if (rules.publicForm && (f.mentions !== undefined || f.images !== undefined))\n return fail(where, `input \"${base.name}\" offers mentions or images; a public form may not read a directory or keep files`);\n if (f.max !== undefined && (!Number.isSafeInteger(f.max) || Number(f.max) < 1 || Number(f.max) > RICH_TEXT_MOST))\n return fail(where, `max must be a number of characters from 1 to ${RICH_TEXT_MOST}`);\n // The words a reader sees, not the markup kept: a body of 20 characters is 20 characters of text.\n if (f.min !== undefined && (!Number.isSafeInteger(f.min) || Number(f.min) < 1 || Number(f.min) > 16384))\n return fail(where, 'min must be a number of characters of text from 1 to 16384');\n let mentions: CommandRichTextMentions | undefined;\n if (f.mentions !== undefined) {\n for (const said of [f.mentions, (f.mentions as Record<string, unknown> | null)?.groups])\n if (said && typeof said === 'object' && Object.hasOwn(said, 'label'))\n throw new Error(`${where}, mentions has \"label\", which is now \"labelColumn\": the column a person is shown by.`);\n const m = record(f.mentions, ['from', 'by', 'labelColumn', 'groups'], `${where}, mentions`);\n /* The teams an @ names as well, as a display rule's `mentions.groups`: a command's\n reply or work note reached only people, though the conversation draws and tells a team. */\n const g =\n m.groups === undefined ? undefined : record(m.groups, ['from', 'by', 'labelColumn', 'through'], `${where}, mentions.groups`);\n mentions = {\n from: name(m.from, `${where}, mentions`, true),\n by: name(m.by, `${where}, mentions`),\n ...(m.labelColumn === undefined ? {} : { labelColumn: mentionLabel(m.labelColumn, `${where}, mentions`) }),\n ...(g\n ? {\n groups: {\n from: name(g.from, `${where}, mentions.groups`, true),\n by: name(g.by, `${where}, mentions.groups`),\n ...(g.labelColumn === undefined ? {} : { labelColumn: mentionLabel(g.labelColumn, `${where}, mentions.groups`) }),\n /* The link rows that put a person in a team, as a display rule's: a team no row\n names has nobody to tell, so the editor does not offer it. */\n ...(g.through === undefined ? {} : { through: name(g.through, `${where}, mentions.groups`, true) }),\n },\n }\n : {}),\n };\n }\n return {\n ...base,\n type: 'richText',\n ...(f.min === undefined ? {} : { minLength: f.min as number }),\n maxLength: (f.max as number | undefined) ?? RICH_TEXT_FIELD_DEFAULT,\n ...(mentions ? { mentions } : {}),\n ...(f.images === undefined ? {} : { images: name(f.images, `${where}, images`, true) }),\n };\n }\n if (f.type === 'number' || f.type === 'money') {\n only(\n f.type === 'money'\n ? ['min', 'max', 'step', 'minor', 'currency', 'below', 'above', 'live']\n : ['min', 'max', 'integer', 'step', 'below', 'above', 'live'],\n );\n if (f.minor !== undefined && typeof f.minor !== 'boolean') return fail(where, 'minor must be boolean');\n if (f.currency !== undefined && (typeof f.currency !== 'string' || !/^[A-Z]{3}$/.test(f.currency)))\n return fail(where, \"currency must be an ISO 4217 code such as 'EUR'\");\n if (f.minor === true && f.step !== undefined)\n return fail(where, 'a minor-unit amount takes its precision from the currency, not a step');\n if (f.type === 'money' && f.step === undefined && f.minor !== true) f.step = 0.01;\n if (f.type === 'money' && f.min === undefined) f.min = 0;\n for (const key of ['min', 'max'] as const)\n if (f[key] !== undefined && (typeof f[key] !== 'number' || !Number.isFinite(f[key]))) fail(where, `${key} must be finite`);\n if (f.min !== undefined && f.max !== undefined && Number(f.min) > Number(f.max)) return fail(where, 'min must not exceed max');\n if (f.integer !== undefined && typeof f.integer !== 'boolean') return fail(where, 'integer must be boolean');\n if (\n f.step !== undefined &&\n (typeof f.step !== 'number' ||\n !Number.isFinite(f.step) ||\n f.step <= 0 ||\n f.step > 1e9 ||\n (f.integer === true && !Number.isSafeInteger(f.step)))\n )\n return fail(where, 'step must be a positive number, whole on an integer field');\n if (base.as === 'slider' && (f.min === undefined || f.max === undefined))\n return fail(where, \"a slider runs between its ends: give min and max, or leave out as: 'slider'\");\n return {\n ...base,\n type: f.type,\n ...(f.min === undefined ? {} : { min: f.min as number }),\n ...(f.max === undefined ? {} : { max: f.max as number }),\n ...(f.integer === undefined ? {} : { integer: f.integer as boolean }),\n ...(f.step === undefined ? {} : { step: f.step as number }),\n ...(f.minor === true ? { minor: true } : {}),\n ...(f.currency === undefined ? {} : { currency: f.currency as string }),\n ...sibling('below'),\n ...sibling('above'),\n ...(f.live === undefined ? {} : { live: liveLines(f.live, where) }),\n } as AppCommandField;\n }\n if (f.type === 'boolean') {\n only(['accept', 'statement']);\n if (f.accept !== undefined && f.accept !== true) return fail(where, 'accept must be true when it is given');\n // A box that must be checked is never left out: optional would make it a yes or a no again.\n if (f.accept === true && f.optional === true) return fail(where, 'a field that must be accepted cannot also be optional');\n // A statement is the words a checkbox stands beside, whether it must be checked or may be left: a consent to texts\n // is as long as the one to a credit check. A yes or a no drawn as two buttons has no box to stand beside.\n if (f.statement !== undefined && base.as === 'buttons')\n return fail(where, \"statement is the words a checkbox stands beside; as: 'buttons' asks a yes or a no, and has none\");\n if (f.accept === true && base.as === 'buttons')\n return fail(where, \"a box that must be checked is a checkbox; as: 'buttons' asks a yes or a no\");\n return {\n ...base,\n type: 'boolean',\n ...(f.accept === true ? { accept: true as const } : {}),\n ...(f.statement === undefined ? {} : { statement: text(f.statement, `${where}, statement`, 2000) }),\n };\n }\n if (f.type === 'month') {\n only(['min', 'max', 'below', 'above']);\n for (const key of ['min', 'max'] as const)\n if (f[key] !== undefined && !isIsoMonth(f[key])) fail(where, `${key} must be a month, YYYY-MM`);\n if (f.min !== undefined && f.max !== undefined && String(f.min) > String(f.max)) return fail(where, 'min must not be after max');\n return {\n ...base,\n type: 'month',\n ...(f.min === undefined ? {} : { min: f.min as string }),\n ...(f.max === undefined ? {} : { max: f.max as string }),\n ...sibling('below'),\n ...sibling('above'),\n };\n }\n if (f.type === 'date') {\n only(['min', 'max', 'below', 'above']);\n for (const key of ['min', 'max'] as const)\n if (f[key] !== undefined && !isIsoDate(f[key])) fail(where, `${key} must be an ISO date, YYYY-MM-DD`);\n if (f.min !== undefined && f.max !== undefined && String(f.min) > String(f.max)) return fail(where, 'min must not be after max');\n return {\n ...base,\n type: 'date',\n ...(f.min === undefined ? {} : { min: f.min as string }),\n ...(f.max === undefined ? {} : { max: f.max as string }),\n ...sibling('below'),\n ...sibling('above'),\n };\n }\n /* A moment: what a person types as a date and a wall time, in the App's time zone, arrives as the UTC instant. */\n if (f.type === 'datetime') {\n only(['min', 'max', 'below', 'above']);\n const bound = (key: 'min' | 'max') => {\n if (f[key] === undefined) return {};\n const at = isoInstant(f[key]);\n return at === undefined || typeof f[key] !== 'string'\n ? fail(where, `${key} must be an ISO 8601 date and time with its zone, such as 2026-10-12T09:00:00Z`)\n : { [key]: at };\n };\n const min = bound('min'),\n max = bound('max');\n if (min.min !== undefined && max.max !== undefined && min.min > max.max) return fail(where, 'min must not be after max');\n return { ...base, type: 'datetime', ...min, ...max, ...sibling('below'), ...sibling('above') };\n }\n if (f.type === 'choice' || f.type === 'choices') {\n only(\n f.type === 'choices' ? ['options', 'from', 'by', 'labelColumn', 'min', 'max', 'exclusive'] : ['options', 'from', 'by', 'labelColumn'],\n );\n const source = f.from === undefined ? undefined : choiceSource(f, where);\n if (!source && (f.by !== undefined || f.labelColumn !== undefined))\n return fail(where, 'by and labelColumn read choices from a table: give from');\n const options = source\n ? []\n : items(f.options, where, 'options', COMMAND_INPUT_LIMITS.choiceOptions, 'options').map((option, index) =>\n choiceOption(option, `${where}, option ${index + 1}`),\n );\n if (!source && !options.length) return fail(where, 'a choice needs at least one option');\n if (new Set(options.map((option) => `${typeof option.value}:${option.value}`)).size !== options.length)\n return fail(where, 'duplicate choice value');\n if (f.type === 'choice') return { ...base, type: 'choice', options, ...(source ? { source } : {}) };\n const cap = COMMAND_INPUT_LIMITS.choiceOptions;\n for (const bound of ['min', 'max'] as const)\n if (\n f[bound] !== undefined &&\n (!Number.isSafeInteger(f[bound]) || Number(f[bound]) < (bound === 'max' ? 1 : 0) || Number(f[bound]) > cap)\n )\n return fail(where, `${bound} must be a whole number of choices up to ${cap}`);\n if (f.min !== undefined && f.max !== undefined && Number(f.min) > Number(f.max)) return fail(where, 'min must not exceed max');\n if (f.min !== undefined && Number(f.min) > 0 && f.optional === true)\n return fail(where, 'a field that must have a choice (min) is not optional');\n let exclusive: (string | number)[] | undefined;\n if (f.exclusive !== undefined) {\n exclusive = items(f.exclusive, `${where}, exclusive`, 'exclusive', cap, 'values') as (string | number)[];\n if (!exclusive.length) return fail(where, 'exclusive names one or more of the options that stand alone');\n if (!source && exclusive.some((value) => !options.some((option) => option.value === value)))\n return fail(where, 'exclusive names a value that is not one of the options');\n }\n return {\n ...base,\n type: 'choices',\n options,\n ...(source ? { source } : {}),\n ...(f.min === undefined ? {} : { min: f.min as number }),\n ...(f.max === undefined ? {} : { max: f.max as number }),\n ...(exclusive ? { exclusive } : {}),\n };\n }\n /* On a public form too: its choices are what `auth.public` opens, read signed out, and the App's public grant is\n judged against it where both are known (forms/savedFormChecks.ts, [public-form-picker]). */\n if (f.type === 'reference') {\n only(['from', 'by', 'labelColumn', 'query']);\n return {\n ...base,\n type: 'reference',\n from: name(f.from, where, true),\n by: name(f.by, where),\n labelColumn: name(f.labelColumn, where),\n ...(f.query === undefined ? {} : { query: text(f.query, where, 2000) }),\n };\n }\n /* Answers to questions that are rows: the rows of `from` an administrator edits while the App runs,\n those `whose` names the record by and those that name none. One object of answers, judged against those rows\n where the command runs; the form draws them once prepare has read them. A public form has no reader to read\n them for, and a group's rows would each ask the same questions again. */\n if (f.type === 'defined') {\n only(['from', 'whose']);\n if (rules.publicForm) return fail(where, `input \"${base.name}\" asks questions defined in rows; a public form may not read them`);\n if (rules.depth) return fail(where, 'questions defined in rows are a field of the input itself, not one inside a group');\n // Its rows say what each answer starts as and whether it is asked; the field itself is always one object.\n if (f.default !== undefined || f.when !== undefined)\n return fail(where, 'questions defined in rows take no default and no when; their rows say what is asked');\n return {\n ...base,\n type: 'defined',\n from: name(f.from, `${where}, from`, true),\n ...(f.whose === undefined ? {} : { whose: name(f.whose, `${where}, whose`) }),\n };\n }\n /* A record's lines, as the lines grid asks them (CommandLinesField): rows of a resource the command writes, each\n the columns it shows, read as their kinds where the command runs. The grid reads the catalog and offers pickers,\n which a public form has neither of; and a line is already a row, so it sits in no group. */\n if (f.type === 'lines') {\n only(['from', 'show', 'copy', 'query', 'min', 'max', 'description']);\n if (f.optional !== undefined || f.default !== undefined || f.when !== undefined)\n return fail(where, 'lines take no optional, default or when; a min of 0 lets the form send none');\n if (rules.publicForm) return fail(where, `input \"${base.name}\" asks lines; a public form may not read a directory`);\n if (rules.depth) return fail(where, 'lines are a field of the input itself, not one inside a group');\n const show = items(f.show, `${where}, show`, 'show', 12, 'columns').map((column) => name(column, `${where}, show`));\n if (!show.length || new Set(show).size !== show.length) return fail(where, \"show names one to 12 of the row's columns, each once\");\n let copy: Record<string, string> | undefined;\n if (f.copy !== undefined) {\n const c = f.copy;\n if (!c || typeof c !== 'object' || Array.isArray(c) || ![null, Object.prototype].includes(Object.getPrototypeOf(c)))\n return fail(where, \"copy is an object of a line's column → the reference it is copied through and that record's column\");\n const entries = Object.entries(c);\n if (!entries.length || entries.length > 8) return fail(where, 'copy fills one to 8 columns');\n copy = {};\n for (const [column, path] of entries) {\n name(column, `${where}, copy`);\n const [via, read, more] = typeof path === 'string' ? path.split('.') : [];\n if (more !== undefined || !via || !read || !identifier.test(via) || !identifier.test(read))\n return fail(\n `${where}, copy \"${column}\"`,\n \"is a reference the line shows and the column of the record it names: 'variant_id.price_amount'\",\n );\n if (!show.includes(via)) return fail(`${where}, copy \"${column}\"`, `is copied through \"${via}\", which the line does not show`);\n if (show.includes(column)) return fail(`${where}, copy \"${column}\"`, 'is asked in show; a copied column is filled, not typed');\n copy[column] = path as string;\n }\n }\n let query: Record<string, string> | undefined;\n if (f.query !== undefined) {\n const q = f.query;\n if (!q || typeof q !== 'object' || Array.isArray(q) || ![null, Object.prototype].includes(Object.getPrototypeOf(q)))\n return fail(where, \"query is an object of a shown reference → the rows its picker offers: { variant_id: 'active = true' }\");\n const entries = Object.entries(q);\n if (!entries.length) return fail(where, 'query narrows one or more of the shown references');\n query = {};\n for (const [column, condition] of entries) {\n if (!show.includes(column)) return fail(`${where}, query \"${column}\"`, 'names a column the line does not show');\n const said = text(condition, `${where}, query \"${column}\"`, 2000);\n /* A line has no one record to read: the form's lines are asked before there is one. */\n if (recordColumnsIn(said).length)\n return fail(`${where}, query \"${column}\"`, 'reads record.<column>; a line has no one record to read it from');\n query[column] = said;\n }\n }\n const cap = COMMAND_INPUT_LIMITS.repeatRows;\n for (const bound of ['min', 'max'] as const)\n if (\n f[bound] !== undefined &&\n (!Number.isSafeInteger(f[bound]) || Number(f[bound]) < (bound === 'max' ? 1 : 0) || Number(f[bound]) > cap)\n )\n return fail(where, `${bound} must be a whole number of lines up to ${cap}`);\n if (f.min !== undefined && f.max !== undefined && Number(f.min) > Number(f.max)) return fail(where, 'min must not exceed max');\n return {\n name: base.name,\n label: base.label,\n ...(base.help === undefined ? {} : { help: base.help }),\n type: 'lines',\n from: name(f.from, where, true),\n show,\n ...(copy ? { copy } : {}),\n ...(query ? { query } : {}),\n ...(f.min === undefined ? {} : { min: f.min as number }),\n max: (f.max as number | undefined) ?? cap,\n };\n }\n if (f.type === 'group') {\n only(['fields', 'repeat', 'description']);\n // A group is always present — rows, or an object — so what may be absent is its members.\n if (f.optional !== undefined) return fail(where, 'a group is never optional; its fields are, or its repeat has no min');\n if (rules.depth >= COMMAND_INPUT_LIMITS.depth) return fail(where, `groups nest at most ${COMMAND_INPUT_LIMITS.depth} deep`);\n let repeat: { min?: number; max: number } | undefined;\n if (f.repeat !== undefined) {\n const r = record(f.repeat, ['min', 'max'], `${where}, repeat`);\n const cap = rules.publicForm ? COMMAND_INPUT_LIMITS.publicRepeatRows : COMMAND_INPUT_LIMITS.repeatRows;\n if (!Number.isSafeInteger(r.max) || Number(r.max) < 1 || Number(r.max) > cap) {\n return fail(where, `repeat max must be an integer from 1 to ${cap}${rules.publicForm ? ' on a public form' : ''}`);\n }\n if (r.min !== undefined && (!Number.isSafeInteger(r.min) || Number(r.min) < 0 || Number(r.min) > Number(r.max))) {\n return fail(where, 'repeat min must be an integer from 0 to its max');\n }\n repeat = { ...(r.min === undefined ? {} : { min: r.min as number }), max: r.max as number };\n }\n const fields = fieldList(f.fields, where, { ...rules, depth: rules.depth + 1 });\n if (!fields.length) return fail(where, 'a group needs at least one field');\n return {\n name: base.name,\n label: base.label,\n ...(base.help === undefined ? {} : { help: base.help }),\n type: 'group',\n fields,\n ...(repeat ? { repeat } : {}),\n };\n }\n return fail(where, 'unknown command input type');\n}\n\n/* Fields keyed by name, in the order written: the object's own keys, each a\n plain data property. A field below or above another names a sibling of\n its own kind, beside it in this list. */\nfunction fieldList(raw: unknown, where: string, rules: FieldRules): AppCommandField[] {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw) || ![null, Object.prototype].includes(Object.getPrototypeOf(raw))) {\n return fail(where, 'input must be an object of fields keyed by name');\n }\n const keys = Reflect.ownKeys(raw);\n if (keys.length > COMMAND_INPUT_LIMITS.listFields) return fail(where, `input holds at most ${COMMAND_INPUT_LIMITS.listFields} fields`);\n const list = keys.map((key) => {\n const descriptor = typeof key === 'string' ? Object.getOwnPropertyDescriptor(raw, key) : undefined;\n if (typeof key !== 'string' || !descriptor?.enumerable || !('value' in descriptor))\n return fail(where, 'input must contain only enumerable data fields');\n return field(name(key, where), descriptor.value, `${where}, input \"${key}\"`, rules);\n });\n const byName = new Map(list.map((item) => [item.name, item]));\n const sensitiveField = (named: string) => {\n const other = byName.get(named);\n return other?.type === 'text' && other.sensitive !== undefined;\n };\n for (const item of list) {\n if (item.type !== 'group' && item.type !== 'lines' && item.when !== undefined) {\n for (const named of rules.names(item.when, where).fields) {\n const other = byName.get(named);\n if (other === item) return fail(where, `input \"${item.name}\" is asked when \"${named}\"…, which is itself`);\n if (!other) {\n /* Before rc.1 a bare name was the record's column; now it is a field of the input. Said with the rewrite. */\n const unknown = rules.names(item.when, where).fields.filter((word) => !byName.has(word));\n const rewrite = unknown.reduce(\n (text, word) => text.replace(new RegExp(`(?<![\\\\w.'\"])${word}(?![\\\\w'\"])`, 'g'), `record.${word}`),\n item.when,\n );\n return fail(\n where,\n `input \"${item.name}\" is asked when \"${named}\"…, which is not a field of this input. A bare name in a field's when is ` +\n `another field of the input; the record's own column is record.${named}: when: ${JSON.stringify(rewrite)}`,\n );\n }\n if (sensitiveField(named))\n return fail(where, `input \"${item.name}\" is asked when \"${named}\"…, a sensitive field; nothing reads a sensitive value`);\n if (\n other.type === 'group' ||\n other.type === 'lines' ||\n other.type === 'richText' ||\n other.type === 'choices' ||\n other.type === 'defined'\n )\n return fail(where, `input \"${item.name}\" is asked when \"${named}\"…, a ${other.type} field a condition cannot compare`);\n }\n }\n if ((item.type === 'number' || item.type === 'money') && item.live) {\n for (const [word, expression] of Object.entries(item.live)) {\n for (const named of liveNames(expression)) {\n const other = byName.get(named);\n if (!other || (other.type !== 'number' && other.type !== 'money'))\n return fail(where, `input \"${item.name}\" live \"${word}\" reads \"${named}\", which is not a number field of this input`);\n }\n }\n }\n for (const rule of ['below', 'above'] as const) {\n const other = (item as { below?: string; above?: string })[rule];\n if (other === undefined) continue;\n const sibling = list.find((candidate) => candidate.name === other);\n if (\n !sibling ||\n sibling === item ||\n (sibling.type !== item.type && !(['number', 'money'].includes(sibling.type) && ['number', 'money'].includes(item.type)))\n ) {\n return fail(where, `input \"${item.name}\" is ${rule} \"${other}\", which must be another ${item.type} field beside it`);\n }\n }\n }\n return list;\n}\n\n/** The field parser behind parseCommandInput (commandInputParse.ts), which hands it the condition compiler's reading. */\nexport function parseCommandFields(\n raw: unknown,\n where: string,\n options: { publicForm?: boolean; names: ConditionNames },\n): AppCommandField[] {\n return fieldList(raw, where, { depth: 0, publicForm: options.publicForm === true, count: { fields: 0 }, names: options.names });\n}\n\n/**\n * What the server knows of a command, for an App that names it: its\n * registration's contract, as the browser is served it. The App writes the\n * name alone (`command: 'assignReviewer'`); the definition lives in the\n * server-only commands module, and loading the App fills each name in from\n * here — so an input is declared once, where the handler reads it.\n */\nexport interface CommandCatalogEntry {\n readonly enabledBy?: import('../../schema/command.js').AppFlagGate;\n /** The contract's version: authority.lock's, 1 until a breaking change is bumped. */\n readonly version: number;\n /** The logical resource it acts on, or inserts into when it names no record. */\n readonly target: string;\n /** How many existing records a request names: one, a selection, or none (a form's command). */\n readonly targets: 'one' | 'many' | 'none';\n readonly realm?: 'tenant' | 'platform';\n /** Run by a public form, and by nothing else. */\n readonly public?: true;\n /** What must hold of the record, in the query language: the command's own `when`, which its actions carry. */\n readonly when?: string;\n readonly input: readonly AppCommandField[];\n /** Its rules over the draft (`rules`), which a form checks step by step and the server again as it runs. */\n readonly rules?: readonly DraftRule[];\n /** A form's command whose form prepares its starting values (`initial`, or `default: me`). */\n readonly prepares?: true;\n}\nexport type CommandCatalog = ReadonlyMap<string, CommandCatalogEntry>;\n\n/* The catalog the App being parsed names its commands from. Parsing is\n synchronous and one App at a time, so the loader sets it around the parse\n (withCommandCatalog) rather than threading it through every block's parser. */\nlet catalog: CommandCatalog | undefined;\n\n/** Whether a word names one of the App's commands, in the catalog the App is being parsed with. */\nexport const isRegisteredCommand = (name: string): boolean => Boolean(catalog?.has(name));\n\n/** A registered command's catalog entry, or undefined for a name the catalog lacks. */\nexport const registeredCommand = (name: string): CommandCatalogEntry | undefined => catalog?.get(name);\n\n/** Check every registered command, including commands not drawn in this App. */\nexport function checkCommandCatalogFlagGates(flags: FlagCatalog): void {\n for (const entry of catalog?.values() ?? []) {\n const gate = commandGate(entry);\n if (gate.enabledBy) normalizeAppFlagGate(gate.enabledBy, flags);\n }\n}\n\nfunction commandGate(entry: CommandCatalogEntry): Pick<AppCommandReference, 'enabledBy'> {\n if (!entry || typeof entry !== 'object' || ![Object.prototype, null].includes(Object.getPrototypeOf(entry)))\n throw new Error('Command catalog entries require plain data.');\n const descriptor = Object.getOwnPropertyDescriptor(entry, 'enabledBy');\n if (!descriptor) return {};\n if (!descriptor.enumerable || !('value' in descriptor)) throw new Error('Command enabledBy must be an own data property.');\n return { enabledBy: normalizeAppFlagGate(descriptor.value) };\n}\n\n/** Run `parse` with the App's commands to resolve names from; a name met with none loaded is refused, naming the flag. */\nexport function withCommandCatalog<T>(commands: CommandCatalog | undefined, parse: () => T): T {\n const before = catalog;\n catalog = commands;\n try {\n return parse();\n } finally {\n catalog = before;\n }\n}\n\n/** The catalog entry an authored name stands for, or the refusal that says why there is none. */\nfunction registered(raw: unknown, where: string): [string, CommandCatalogEntry] {\n if (typeof raw !== 'string') {\n return fail(where, \"names its command by name, as the commands module registers it: command: 'assignReviewer'\");\n }\n if (!catalog) {\n return fail(\n where,\n `names the command \"${raw}\", and no commands module is loaded. Give the App its commands module with --commands <file>, ` +\n \"or keep it as commands.ts beside the App's manifest\",\n );\n }\n const entry = catalog.get(raw);\n if (!entry) {\n const known = [...catalog.keys()];\n return fail(\n where,\n `names the command \"${raw}\", which the commands module does not register${known.length ? ` (${unknownKey(raw, known).replace(/^has no \"[^\"]*\"\\.\\s*/, '')})` : ''}`,\n );\n }\n return [raw, entry];\n}\n\nconst served = (input: readonly AppCommandField[]): AppCommandField[] => structuredClone(input) as AppCommandField[];\n\n/** A command an action or an import runs on an existing record: its name, resolved to what the browser is served. */\nexport function parseCommandReference(raw: unknown, where: string): AppCommandReference {\n const [id, entry] = registered(raw, where);\n if (entry.targets === 'none')\n return fail(where, `names \"${id}\", which names no existing record (it is a form's command); run it from commandForm`);\n return {\n id,\n version: entry.version,\n target: entry.target,\n ...(entry.targets === 'many' ? { targets: 'many' as const } : {}),\n ...(entry.realm === undefined ? {} : { realm: entry.realm }),\n ...commandGate(entry),\n input: served(entry.input),\n ...(entry.rules?.length ? { rules: structuredClone(entry.rules) as DraftRule[] } : {}),\n };\n}\n\n/** A command an API exposes, by name: one on a record, or a signed-in form's, as its registration says. */\nexport function parseExposedCommandReference(raw: unknown, where: string): AppCommandReference | AppFormCommandReference {\n const [, entry] = registered(raw, where);\n return entry.targets === 'none' ? parseFormCommandReference(raw, where, false) : parseCommandReference(raw, where);\n}\n\n/**\n * The command a form runs, by name: one that names no existing record. A\n * public form's must be registered `public: true`, and a signed-in form's\n * must not: a public command runs only for whoever submits a public form.\n */\nexport function parseFormCommandReference(raw: unknown, where: string, publicForm: boolean): AppFormCommandReference {\n const [id, entry] = registered(raw, where);\n if (entry.targets !== 'none')\n return fail(\n where,\n `names \"${id}\", which acts on an existing record; a form's command creates records and names none (targets: 'none')`,\n );\n if (publicForm && entry.public !== true) return fail(where, `is public and names \"${id}\", which is not registered with public: true`);\n if (!publicForm && entry.public === true) {\n return fail(\n where,\n `is signed in and names \"${id}\", which is registered with public: true. A public command runs only from a public form (public: true)`,\n );\n }\n return {\n id,\n version: entry.version,\n ...commandGate(entry),\n input: served(entry.input),\n ...(entry.rules?.length ? { rules: structuredClone(entry.rules) as DraftRule[] } : {}),\n ...(entry.prepares ? { prepares: true } : {}),\n };\n}\n\n/** Schema authoring checks; command registration and permission checks are server-owned. */\nexport function commandReferenceProblem(command: AppCommandReference, owner: string, schema: DatabaseSchema): string | undefined {\n const target = tableNamed(schema, command.target);\n if (!target || target.id !== owner) return 'Command target must match the action’s owning resource.';\n if (target.isView || !target.columns.some((column) => column.primaryKey)) return 'Command target must be a table with a primary key.';\n return commandInputProblem(command.input, schema, { table: target, many: command.targets === 'many' });\n}\n\n/* `record.<column>` in a picker's query: the column of the record the command\n acts on, the word a timeline's `side` already reads it by. */\nconst RECORD_WORD = /(?<![\\w$.])record\\.([A-Za-z_]\\w*)/g;\n\n/** The record's columns a reference input's query names (`sales_order_id = record.id`). */\nexport const recordColumnsIn = (query: string): string[] => [...maskQuoted(query).matchAll(RECORD_WORD)].map((m) => m[1]);\n\n/**\n * A picker's query with the record's values in place of its `record.<column>`\n * words, as prepare serves it: null when one of them is empty (or not the\n * reader's to see), which offers nothing rather than every row.\n */\nexport function boundRecordQuery(query: string, row: Readonly<Record<string, unknown>>): string | null {\n let out = query;\n for (const m of [...maskQuoted(query).matchAll(RECORD_WORD)].reverse()) {\n const value = row[m[1]];\n if (value === null || value === undefined) return null;\n out = `${out.slice(0, m.index)}${quoteValue(value)}${out.slice(m.index + m[0].length)}`;\n }\n return out;\n}\n\n/* A value of the column's kind, so a query that names the record is judged as it will read once bound. */\nconst SAMPLE = { number: 1, boolean: true, date: '2000-01-01', text: 'x', other: 'x' } as const;\n\n/* Questions defined in rows: rows with the columns a row-defined field's have, and `whose` a column of\n them that holds the record's one key. */\nfunction definedInputProblem(\n f: Extract<AppCommandField, { type: 'defined' }>,\n schema: DatabaseSchema,\n target: { table: TableMeta; many: boolean } | undefined,\n): string | undefined {\n const from = tableNamed(schema, f.from);\n if (!from) return `Command input \"${f.name}\" asks the questions in \"${f.from}\", which is not a resource the command reads.`;\n const missing = ['key', 'label', 'type'].filter((name) => !from.columns.some((column) => column.name === name));\n if (missing.length)\n return `Command input \"${f.name}\": \"${f.from}\" has no ${missing.map((name) => `\"${name}\"`).join(', ')}; a question row has key, label and type (and may have options, required, help and position).`;\n if (f.whose === undefined) return undefined;\n if (!from.columns.some((column) => column.name === f.whose))\n return `Command input \"${f.name}\": \"${f.from}\" has no column \"${f.whose}\" for whose.`;\n if (!target || target.many) return `Command input \"${f.name}\" picks its questions by the record (whose), and a form's command has none.`;\n if (target.table.columns.filter((column) => column.primaryKey).length !== 1)\n return `Command input \"${f.name}\" picks its questions by the record's key, and ${target.table.id} has no single-column key.`;\n return undefined;\n}\n\n/* A record's lines: rows of a resource in this catalog, each column one a form writes, and each copy through a\n reference the line shows to a column of the record it names. */\nfunction linesInputProblem(f: Extract<AppCommandField, { type: 'lines' }>, schema: DatabaseSchema): string | undefined {\n const from = tableNamed(schema, f.from);\n if (!from) return `Command input \"${f.name}\" asks lines of \"${f.from}\", which is not a resource the command writes.`;\n for (const column of [...f.show, ...Object.keys(f.copy ?? {})]) {\n if (!from.columns.some((c) => c.name === column)) return `Command input \"${f.name}\": \"${column}\" is not a column of ${from.id}.`;\n /* Not the column's `writable`: a reader's catalog says that of a table this reader may not write, and the\n lines are written under the command's grant, not the reader's. Only what the App works out is never asked. */\n if (f.show.includes(column) && (from.computed?.[column] || from.rollups?.[column]))\n return `Command input \"${f.name}\": \"${column}\" is worked out, not written by a form.`;\n }\n for (const [column, path] of Object.entries(f.copy ?? {})) {\n const [via, read] = path.split('.');\n const target = from.columns.find((c) => c.name === via)?.references;\n const record = target && tableNamed(schema, target.table);\n if (!record) return `Command input \"${f.name}\" copies \"${column}\" through \"${via}\", which is not a reference of ${from.id}.`;\n if (!record.columns.some((c) => c.name === read))\n return `Command input \"${f.name}\" copies \"${column}\" from \"${read}\", which is not a column of ${record.id}.`;\n }\n for (const [column, condition] of Object.entries(f.query ?? {})) {\n const target = from.columns.find((c) => c.name === column)?.references;\n const record = target && tableNamed(schema, target.table);\n if (!record) return `Command input \"${f.name}\" narrows \"${column}\", which is not a reference of ${from.id}.`;\n /* Read as the picker reads it: a plain query, else a view (a path or a rollup), as a reference's query is. */\n const plain = parseQuery(`${record.id} ${condition}`, { schema, me: ANYONE });\n const view = plain.errors.length ? parseViewQuery(schema, record.id, condition, undefined, { me: ANYONE }) : undefined;\n if (view?.errors.length) return `Command input \"${f.name}\" query \"${column}\": ${view.errors[0].message}`;\n }\n return undefined;\n}\n\n/** The reference inputs a command asks, against the catalog: a form's command has only these to check. */\nexport function commandInputProblem(\n input: readonly AppCommandField[],\n schema: DatabaseSchema,\n target?: { table: TableMeta; many: boolean },\n): string | undefined {\n for (const { field: f } of commandFieldsDeep(input)) {\n const start = f.type === 'group' ? undefined : f.default;\n if (start && typeof start === 'object' && 'record' in start && target && !target.table.columns.some((c) => c.name === start.record))\n return `Command input \"${f.name}\" starts from record.${start.record}, which is not a column of ${target.table.id}.`;\n if (f.type === 'defined') {\n const problem = definedInputProblem(f, schema, target);\n if (problem) return problem;\n continue;\n }\n if (f.type === 'lines') {\n const problem = linesInputProblem(f, schema);\n if (problem) return problem;\n continue;\n }\n if ((f.type === 'choice' || f.type === 'choices') && f.source) {\n const from = tableNamed(schema, f.source.from);\n if (!from)\n return `Command input \"${f.name}\" offers the rows of \"${f.source.from}\", which is not a resource the command reads: add it to \\`read\\`.`;\n for (const column of [f.source.by, f.source.labelColumn])\n if (!from.columns.some((c) => c.name === column)) return `Command input \"${f.name}\": \"${f.source.from}\" has no column \"${column}\".`;\n continue;\n }\n if (f.type !== 'reference') continue;\n const resource = tableNamed(schema, f.from);\n if (!resource || !resource.columns.some((column) => column.name === f.labelColumn))\n return (\n `Command input \"${f.name}\" has an unresolved reference resource or label column. ` +\n (resource\n ? `\"${f.from}\" has no column \"${f.labelColumn}\": name one it has.`\n : `Most likely \"${f.from}\" is not in the command's \\`read\\` (a picker reads only the tables the command reads): add \"${f.from}\" to \\`read\\`, or check the name.`)\n );\n const keys = resource.columns.filter((column) => column.primaryKey);\n if (keys.length !== 1 || keys[0].name !== f.by) return `Command input \"${f.name}\" must select a single primary key.`;\n if (f.query) {\n const named = recordColumnsIn(f.query);\n if (named.length && (!target || target.many))\n return `Command input \"${f.name}\" query names record.${named[0]}, but ${target ? \"a command that acts on many records (targets: 'many')\" : \"a form's command\"} has no one record to read it from.`;\n const columns = named.map((name) => target!.table.columns.find((column) => column.name === name));\n const unknown = named.find((_, i) => !columns[i]);\n if (unknown) return `Command input \"${f.name}\" query names record.${unknown}, which is not a column of ${target!.table.id}.`;\n const asked = named.length\n ? boundRecordQuery(f.query, Object.fromEntries(columns.map((column) => [column!.name, SAMPLE[columnKind(column!.type)]])))!\n : f.query;\n /* The picker's read takes the query as a view where it is not a plain\n one (the /api/run fall-through), so a path or a rollup — \"released\n = 0\" on an order — is judged as that read will judge it. */\n // Judged before any request: the picker's read asks it as the reader, so `me` here is anyone's.\n const query = parseQuery(`${resource.id} ${asked}`, { schema, me: ANYONE });\n const view = query.errors.length ? parseViewQuery(schema, resource.id, asked, undefined, { me: ANYONE }) : undefined;\n if (view?.errors.length) return `Command input \"${f.name}\" query: ${view.errors[0].message}`;\n }\n }\n return undefined;\n}\n","/**\n * An expression's words read into its tree (computed.ts): the tokenizer and the parser computed columns, a form's\n * live lines and the App's checks read with. A module of its own because nothing in computed.ts calls it, and a\n * browser's first page reaches computed.ts (a computed column's meta): what a part loaded on demand uses of a module\n * the first page reaches is drawn into the first page's chunk, so the parser lived there for a form's live lines.\n */\nimport { ComputedError, type ComputedFunction, type ComputedNode, type ComputedOp, type ComputedUnit } from './computed.js';\n\n/** The functions an expression may call: each written alike on every engine and worked out alike in JavaScript. */\nconst COMPUTED_FUNCTIONS = ['coalesce', 'round', 'min', 'max', 'abs'] as const satisfies readonly ComputedFunction[];\nconst isFunction = (word: string): word is ComputedFunction => (COMPUTED_FUNCTIONS as readonly string[]).includes(word);\n/* The greatest number of places `round` keeps: past it a figure is no longer one an engine says exactly. */\nconst ROUND_PLACES = 10;\n\nconst CONSTANTS = new Set(['today', 'now', 'me', 'true', 'false']);\n\ntype Token = { t: 'word' | 'number' | 'text' | 'op' | 'end'; v: string; at: number };\n\nconst UNITS: Record<string, ComputedUnit> = {\n minute: 'minute',\n minutes: 'minute',\n hour: 'hour',\n hours: 'hour',\n day: 'day',\n days: 'day',\n week: 'week',\n weeks: 'week',\n month: 'month',\n months: 'month',\n year: 'year',\n years: 'year',\n};\nconst WORD = /[A-Za-z_][A-Za-z0-9_]*(?:\\.[A-Za-z_$][A-Za-z0-9_$]*)*/y;\n\nfunction tokens(text: string): Token[] {\n const out: Token[] = [];\n let i = 0;\n while (i < text.length) {\n const c = text[i];\n if (/\\s/.test(c)) {\n i++;\n continue;\n }\n if (/\\d/.test(c)) {\n const m = /\\d+(?:\\.\\d+)?/y;\n m.lastIndex = i;\n const n = m.exec(text)![0];\n out.push({ t: 'number', v: n, at: i });\n i += n.length;\n continue;\n }\n if (c === \"'\") {\n let j = i + 1,\n value = '';\n for (;;) {\n if (j >= text.length) throw new ComputedError('a quoted text is not closed.', i);\n if (text[j] === \"'\") {\n if (text[j + 1] === \"'\") {\n value += \"'\";\n j += 2;\n continue;\n }\n break;\n }\n value += text[j++];\n }\n /* Written into the statement between quotes, so held to what every engine reads alike there (recordLabel.ts). */\n if ([...value].some((ch) => ch === '\\\\' || ch.charCodeAt(0) < 0x20 || ch.charCodeAt(0) === 0x7f)) {\n throw new ComputedError('a quoted text holds a backslash or a control character, which engines read differently.', i);\n }\n out.push({ t: 'text', v: value, at: i });\n i = j + 1;\n continue;\n }\n const two = text.slice(i, i + 2);\n if (['!=', '<>', '<=', '>='].includes(two)) {\n out.push({ t: 'op', v: two === '<>' ? '!=' : two, at: i });\n i += 2;\n continue;\n }\n if ('=<>+-*/(),'.includes(c)) {\n out.push({ t: 'op', v: c, at: i });\n i++;\n continue;\n }\n WORD.lastIndex = i;\n const w = WORD.exec(text);\n if (w) {\n out.push({ t: 'word', v: w[0], at: i });\n i += w[0].length;\n continue;\n }\n throw new ComputedError(`\"${c}\" is not part of an expression.`, i);\n }\n out.push({ t: 'end', v: '', at: text.length });\n return out;\n}\n\n/**\n * An expression's tree, or why it is not one. `isColumn` says which bare\n * words are names: a word on the right of `=` or `!=` that is not one is a\n * value, as it is in a query (`status = open`).\n */\nexport function parseComputed(text: string, isColumn: (path: string) => boolean): ComputedNode {\n const list = tokens(text);\n let p = 0;\n const peek = () => list[p];\n const word = (v: string) => peek().t === 'word' && peek().v.toLowerCase() === v;\n const take = () => list[p++];\n const expect = (v: string) => {\n if (peek().t !== 'op' || peek().v !== v)\n throw new ComputedError(`expected \"${v}\"${peek().t === 'end' ? ' before the end' : ` at \"${peek().v}\"`}.`, peek().at);\n take();\n };\n const or = (): ComputedNode => {\n let left = and();\n while (word('or')) {\n take();\n left = { k: 'op', op: 'or', left, right: and() };\n }\n return left;\n };\n const and = (): ComputedNode => {\n let left = not();\n while (word('and')) {\n take();\n left = { k: 'op', op: 'and', left, right: not() };\n }\n return left;\n };\n const not = (): ComputedNode => {\n if (word('not')) {\n take();\n return { k: 'not', of: not() };\n }\n return compare();\n };\n const compare = (): ComputedNode => {\n const left = additive();\n if (word('is')) {\n take();\n const negate = word('not');\n if (negate) take();\n if (!word('empty')) throw new ComputedError('say \"is empty\" or \"is not empty\".', peek().at);\n take();\n return { k: 'empty', of: left, negate };\n }\n /* `status in (open, waiting)`, `status not in (done)`: the query language's list, said as the equalities it\n means, joined by \"or\", so it types, compiles on every engine and works out in the browser as `=` does. */\n const negated = word('not') && list[p + 1].t === 'word' && list[p + 1].v.toLowerCase() === 'in';\n if (negated || word('in')) {\n if (negated) take();\n take();\n expect('(');\n const items: ComputedNode[] = [];\n for (;;) {\n const valueAt = peek();\n const next = list[p + 1];\n items.push(\n left.k === 'path' &&\n valueAt.t === 'word' &&\n !CONSTANTS.has(valueAt.v.toLowerCase()) &&\n !valueAt.v.includes('.') &&\n !isColumn(valueAt.v) &&\n next.t === 'op' &&\n (next.v === ',' || next.v === ')')\n ? (take(), { k: 'text', value: valueAt.v })\n : additive(),\n );\n if (peek().t === 'op' && peek().v === ',') {\n take();\n continue;\n }\n expect(')');\n break;\n }\n const any = items\n .map((right): ComputedNode => ({ k: 'op', op: '=', left, right }))\n .reduce((either, each): ComputedNode => ({ k: 'op', op: 'or', left: either, right: each }));\n return negated ? { k: 'not', of: any } : any;\n }\n const op = peek();\n if (op.t === 'op' && ['=', '!=', '<', '<=', '>', '>='].includes(op.v)) {\n take();\n const valueAt = peek();\n /* `status = open`: a bare word that names no column is the value, as in a query. */\n const right =\n (op.v === '=' || op.v === '!=') &&\n left.k === 'path' &&\n valueAt.t === 'word' &&\n !CONSTANTS.has(valueAt.v.toLowerCase()) &&\n !valueAt.v.includes('.') &&\n !isColumn(valueAt.v) &&\n (list[p + 1].t === 'end' || list[p + 1].t === 'word' || (list[p + 1].t === 'op' && list[p + 1].v === ')'))\n ? (take(), { k: 'text', value: valueAt.v } as ComputedNode)\n : additive();\n if (op.v === '=' && right.k === 'path' && /^null$/i.test(right.path))\n throw new ComputedError('\"= null\" is never true; say \"is empty\".', valueAt.at);\n return { k: 'op', op: op.v as ComputedOp, left, right };\n }\n return left;\n };\n const additive = (): ComputedNode => {\n let left = multiplicative();\n while (peek().t === 'op' && (peek().v === '+' || peek().v === '-')) {\n const op = take().v as ComputedOp;\n left = { k: 'op', op, left, right: multiplicative() };\n }\n return left;\n };\n const multiplicative = (): ComputedNode => {\n let left = unary();\n while (peek().t === 'op' && (peek().v === '*' || peek().v === '/')) {\n const op = take().v as ComputedOp;\n left = { k: 'op', op, left, right: unary() };\n }\n return left;\n };\n const unary = (): ComputedNode => {\n if (peek().t === 'op' && peek().v === '-') {\n take();\n return { k: 'neg', of: unary() };\n }\n const value = primary();\n /* `3 days`, `term months`: a span a date moves by. */\n if (peek().t === 'word' && Object.hasOwn(UNITS, peek().v.toLowerCase()))\n return { k: 'span', of: value, unit: UNITS[take().v.toLowerCase()] };\n return value;\n };\n const primary = (): ComputedNode => {\n const token = take();\n if (token.t === 'number') return { k: 'number', value: Number(token.v) };\n if (token.t === 'text') return { k: 'text', value: token.v };\n if (token.t === 'op' && token.v === '(') {\n const inner = or();\n expect(')');\n return inner;\n }\n if (token.t === 'word') {\n const lower = token.v.toLowerCase();\n /* `round(total, 2)`: a function's name is a word followed by \"(\", which no column or value ever is. */\n if (peek().t === 'op' && peek().v === '(') {\n if (!isFunction(lower))\n throw new ComputedError(`\"${token.v}\" is not a function; an expression calls ${COMPUTED_FUNCTIONS.join(', ')}.`, token.at);\n take();\n const args: ComputedNode[] = [];\n if (!(peek().t === 'op' && peek().v === ')'))\n for (;;) {\n args.push(or());\n if (peek().t === 'op' && peek().v === ',') {\n take();\n continue;\n }\n break;\n }\n expect(')');\n const fn = lower;\n const many = fn === 'coalesce' || fn === 'min' || fn === 'max';\n if (many ? args.length < 2 : fn === 'round' ? args.length < 1 || args.length > 2 : args.length !== 1)\n throw new ComputedError(\n many\n ? `${fn}(…) takes two values or more: ${fn}(a, b).`\n : fn === 'round'\n ? 'round(…) takes a number and, if you like, how many places: round(total) or round(total, 2).'\n : `${fn}(…) takes one number.`,\n token.at,\n );\n if (fn === 'round' && args[1] && !(args[1].k === 'number' && Number.isInteger(args[1].value) && args[1].value <= ROUND_PLACES))\n throw new ComputedError(\n `round(…)'s places are a whole number from 0 to ${ROUND_PLACES}, written out: round(total, 2).`,\n token.at,\n );\n return { k: 'call', fn, args };\n }\n if (lower === 'true' || lower === 'false') return { k: 'bool', value: lower === 'true' };\n if (lower === 'today' || lower === 'now' || lower === 'me') return { k: lower };\n if (['and', 'or', 'not', 'is', 'empty', 'in'].includes(lower) || Object.hasOwn(UNITS, lower))\n throw new ComputedError(`\"${token.v}\" stands where a value goes.`, token.at);\n return { k: 'path', path: token.v };\n }\n throw new ComputedError(\n token.t === 'end' ? 'the expression ends where a value goes.' : `\"${token.v}\" stands where a value goes.`,\n token.at,\n );\n };\n const tree = or();\n if (peek().t !== 'end') throw new ComputedError(`\"${peek().v}\" follows a complete expression; join two with \"and\" or \"or\".`, peek().at);\n return tree;\n}\n","/**\n * A funnel over rows (`funnel()`, a dashboard section): how many of the rows a section holds reach each of its stages,\n * in order. The stages are conditions on the one table; a row reaches stage k when it holds the section's own query and\n * every required stage up to k. An optional stage is counted against the chain before it, and the stages after it do\n * not need it — so one key covers a strict road and an any-order checklist (every stage optional).\n *\n * Every stage is counted in the one statement (`count when [...]`, breakdown.ts): one snapshot, so a later stage can\n * never exceed an earlier one under concurrent writes, which separate counts could.\n *\n * Pure text in, text out: the client draws from it, the server asks from it for a first page (firstPaintRooms.ts), and\n * `mistri check` judges with it.\n */\nimport { andQuery, topLevelConditions } from './query.js';\n\nexport interface FunnelStage {\n /** The stage as a reader says it, said from the catalogs by the time it is parsed. */\n label: string;\n /** Conditions only (segments are words in it); absent, every row the section holds. */\n query?: string;\n /** Counted against the chain before it, and not required by the stages after it. */\n optional?: boolean;\n}\n\nexport const FUNNEL_MIN_STAGES = 2;\nexport const FUNNEL_MAX_STAGES = 8;\n\n/** The answer's column for stage `index` (0-based): the alias its measure is asked under. */\nexport const stageColumn = (index: number): string => `stage_${index + 1}`;\n\n/**\n * The conditions of each stage's reached rows: the required stages before it, then its own. Empty text for a stage\n * that asks nothing of the rows, which are all of them.\n */\nexport function funnelChains(stages: readonly Pick<FunnelStage, 'query' | 'optional'>[]): string[] {\n const required: string[] = [];\n return stages.map((stage) => {\n const chain = andQuery(...required, stage.query);\n if (!stage.optional && stage.query?.trim()) required.push(stage.query);\n return chain;\n });\n}\n\n/**\n * The stage each stage is a share of: the last required stage before it. The first stage has none. A funnel whose\n * first stage is optional is refused where it is parsed (appViews.ts), so every later stage has one.\n */\nexport function funnelBasis(stages: readonly Pick<FunnelStage, 'optional'>[]): Array<number | null> {\n let last: number | null = null;\n return stages.map((stage, index) => {\n const basis = last;\n if (!stage.optional) last = index;\n return basis;\n });\n}\n\n/** The line a funnel asks: its table, the section's conditions, then one count per stage. */\nexport function funnelText(section: { table?: string; query?: string; stages?: readonly FunnelStage[] }): string | null {\n if (!section.table || !section.stages?.length) return null;\n const measures = funnelChains(section.stages).map((chain, index) =>\n chain ? `count when [${chain}] as ${stageColumn(index)}` : `count as ${stageColumn(index)}`,\n );\n return [section.table, section.query, measures.join(', ')].filter(Boolean).join(' ');\n}\n\nconst COMPARE_OPPOSITE: Record<string, string> = { '=': '!=', '!=': '=', '>': '<=', '>=': '<', '<': '>=', '<=': '>' };\n/* A value that is a phrase of the date grammar or a clock word, not a plain value: its opposite is not what `!=` says. */\nconst NOT_PLAIN_VALUE = /^(today|yesterday|tomorrow|now|this|next|last|previous)$/i;\nconst VALUE = String.raw`(\"[^\"]*\"|'[^']*'|[^\\s\"',()]+)`;\n\n/**\n * One condition's opposite, or undefined where the language cannot say it exactly. The opposite is exact for a row\n * with nothing in the column too: `status = approved` is not `status != approved` (a SQL comparison skips an empty\n * value), it is `status != approved or status is empty`. Unsaid on purpose: a phrase of dates, a nested `or`, `has`,\n * anything the language has no word for, since the drill must open the rows the figure counted and no others.\n */\nexport function negateCondition(condition: string): string | undefined {\n const text = condition.replace(/\\s+/g, ' ').trim();\n if (!text || text.startsWith('(')) return undefined;\n const rows = /^(any|no) (.+)$/i.exec(text);\n if (rows) return `${rows[1].toLowerCase() === 'any' ? 'no' : 'any'} ${rows[2]}`;\n const empty = /^([\\w.$+]+) is (not )?empty$/i.exec(text);\n if (empty) return `${empty[1]} is ${empty[2] ? '' : 'not '}empty`;\n const compare = new RegExp(String.raw`^([\\w.$+]+) (=|!=|>=|<=|>|<) ${VALUE}$`).exec(text);\n if (compare && !NOT_PLAIN_VALUE.test(compare[3]))\n return `${compare[1]} ${COMPARE_OPPOSITE[compare[2]]} ${compare[3]} or ${compare[1]} is empty`;\n const among = /^([\\w.$+]+) in \\((.+)\\)$/i.exec(text);\n if (among) {\n const values = among[2].match(new RegExp(String.raw`${VALUE}(?=\\s*(,|$))`, 'g'));\n const whole = new RegExp(String.raw`^\\s*${VALUE}(\\s*,\\s*${VALUE})*\\s*$`);\n if (!values?.length || !whole.test(among[2]) || values.some((value) => NOT_PLAIN_VALUE.test(value))) return undefined;\n return `${values.map((value) => `${among[1]} != ${value}`).join(' and ')} or ${among[1]} is empty`;\n }\n return undefined;\n}\n\n/**\n * The rows that do not hold a query, or undefined when any of its conditions cannot be turned over exactly. A query\n * is its conditions all at once, so its opposite is any one of their opposites.\n */\nexport function negateQuery(query: string, segments?: Readonly<Record<string, string>>): string | undefined {\n const held = topLevelConditions(query, segments);\n if (!held.length) return undefined;\n const opposites = held.map(negateCondition);\n return opposites.every((text): text is string => text !== undefined) ? opposites.join(' or ') : undefined;\n}\n\n/**\n * The rows a stage lost: those that reached the stage it is a share of and do not hold its own conditions. Only a\n * required stage drops rows (an optional one is a side road), and only where its conditions can be turned over.\n */\nexport function droppedQuery(\n stages: readonly FunnelStage[],\n index: number,\n segments?: Readonly<Record<string, string>>,\n): string | undefined {\n const stage = stages[index];\n if (!stage || stage.optional || index === 0 || !stage.query?.trim()) return undefined;\n const opposite = negateQuery(stage.query, segments);\n if (!opposite) return undefined;\n const before = stages.slice(0, index).flatMap((earlier) => (!earlier.optional && earlier.query?.trim() ? [earlier.query] : []));\n return andQuery(...before, opposite);\n}\n","/**\n * A page's archetype: `page(…, { layout })`, the arrangement of its regions.\n *\n * Every record page used to be one skeleton, a main column with an aside a\n * third of its width beside it, and a look could recolour that skeleton but\n * never move it. Three kinds of record read badly in it: one worked by\n * talking (the thread was pushed down by the lists above it), one chosen from\n * photos (on a phone the asking rent and its call to action fell to the\n * bottom, after every list) and one that is mostly words (a full-width column\n * with no measure). Each is a word here. A layout decides which regions a\n * page has, what each may hold and the DOM order; it never sets a colour, a\n * rule or a type size, which stay the looks' recipes.\n *\n * The words are refused where they cannot hold: what a layout cannot place is\n * said by the section and with the fix, so a page never draws an arrangement\n * nobody asked for.\n *\n * A page that names none is given the one its sections already make\n * (chooseLayout): a gallery first is a showcase, a timeline the main column\n * ends with a conversation, a page of the record's own words an article, and\n * anything else the record's skeleton, `'record'`. Chosen when the App is\n * read, from what the page declares, never from a row: a page keeps one shape\n * for every record and from its first frame. Writing a word is still worth\n * it: a written layout refuses a section it cannot place, where a chosen one\n * falls back to the record; `'record'` is how a page that would be chosen as\n * something else keeps the skeleton.\n */\nimport type { PageDef, PageSection } from './pageModel.js';\nimport { isLongText, MAX_PROPERTIES } from './pageAnatomy.js';\n\n/** The archetypes a page is drawn in. The record's own skeleton draws no `data-layout`. */\nexport type PageLayoutName = 'conversation' | 'showcase' | 'article';\nconst PAGE_LAYOUTS: readonly PageLayoutName[] = ['conversation', 'showcase', 'article'];\n/** What a page may say: an archetype, or `'record'`, the skeleton, kept where another would be chosen. */\nexport type PageLayoutWord = 'record' | PageLayoutName;\nexport const PAGE_LAYOUT_WORDS: readonly PageLayoutWord[] = ['record', ...PAGE_LAYOUTS];\n\n/* A showcase's panel stands beside the photos and ends with them: more than three sections there grew the band\n past the gallery's height at a desk, and the body under it stood a screen down. */\nconst PANEL_MOST = 3;\n\nconst WORDS =\n \"'record' (the record's own: its lists beside a rail), 'conversation' (a thread with its facts beside it), 'showcase' (photos with the price beside them) or 'article' (one column at a reading measure)\";\n\n/** `layout`, read: one of the words, else refused with all four. */\nexport function parsePageLayout(raw: unknown, at: string): PageLayoutWord {\n if (PAGE_LAYOUT_WORDS.includes(raw as PageLayoutWord)) return raw as PageLayoutWord;\n /* Renamed before 1.0: \"listing\" is what a list view is called everywhere else in an App. */\n if (raw === 'listing')\n throw new Error(`[page-layout] ${at} has \"layout\": \"listing\", which is now \"showcase\": photos with the price beside them.`);\n throw new Error(\n `[page-layout] ${at} has \"layout\": ${JSON.stringify(raw)}. A page's layout is ${WORDS}; leave it out for the one its sections make.`,\n );\n}\n\n/** The layout a renderer draws: an archetype, or none for the record's own. */\nexport function drawnLayout(page: { layout?: string }): PageLayoutName | undefined {\n return PAGE_LAYOUTS.find((word) => word === page.layout);\n}\n\n/* Structural, so the client's own page shapes (components/pageModel.ts) are read by the same rule. */\ntype Placed = {\n kind: string;\n region?: string;\n presentation?: unknown;\n hidden?: boolean;\n};\nconst isHeader = (section: Placed) => section.kind === 'fields' && section.presentation === 'header';\nconst inMain = (section: Placed) => (section.region ?? 'main') === 'main' && !isHeader(section) && !section.hidden;\nconst inAside = (section: Placed) => section.region === 'aside' && !section.hidden;\n\n/**\n * A showcase's lead: the index of its first main section, the photos, which stand beside the panel. The head\n * is drawn above it, so a header() is not the first.\n */\nexport function showcaseIndex(page: { layout?: string; sections: readonly Placed[] }): number | undefined {\n if (page.layout !== 'showcase') return undefined;\n const index = page.sections.findIndex(inMain);\n return index < 0 ? undefined : index;\n}\n\n/** A section as a refusal names it: its kind and, where it has one, its title. */\nfunction named(section: PageSection): string {\n const title = 'title' in section && typeof section.title === 'string' ? ` \"${section.title}\"` : '';\n if (section.kind === 'fields') {\n const kind = section.presentation === 'card' || section.presentation === 'headline' ? section.presentation : 'fields';\n return `a ${kind}${title}`;\n }\n if (section.kind === 'list') return `${section.view !== undefined ? 'an embed' : 'a related list'}${title}`;\n return `a ${section.kind}${title}`;\n}\n\n/**\n * Judge a parsed App page against its layout, and place what the layout places unasked. `at` names the page as\n * the App's other page refusals do. Mutates `page`: under `conversation` a headline that names no region stands\n * in the aside, as a card and the history already do; under `showcase` and `article` no default rail is drawn.\n */\nexport function applyPageLayout(page: PageDef, at: string): void {\n const layout = drawnLayout(page);\n if (!layout) return;\n const each = page.sections.map((section, index) => ({\n section,\n index,\n where: `${at}, section ${index + 1}`,\n }));\n\n if (layout === 'conversation') {\n const say = (text: string) => new Error(`[layout-conversation] ${text}`);\n const fix = \"A conversation is its one timeline, last in main: put the rest in region: 'aside'\";\n for (const { section } of each) {\n if (section.kind === 'fields' && section.presentation === 'headline' && section.region === undefined) section.region = 'aside';\n }\n const timelines = each.filter(({ section }) => section.kind === 'timeline' && !section.hidden);\n if (!timelines.length)\n throw say(\n `${at} is a conversation with no timeline. ${fix}, and add timeline('Conversation', [stream(…)]) to the main column, or leave out \"layout\".`,\n );\n if (timelines.length > 1)\n throw say(\n `${at} is a conversation with ${timelines.length} timelines. A conversation is its one timeline: say every stream in one timeline('…', [stream(…), stream(…)]).`,\n );\n const thread = timelines[0];\n if (thread.section.region === 'aside')\n throw say(`${thread.where} puts the conversation's timeline in the aside. ${fix}; leave its region out.`);\n for (const { section, where } of each) {\n if (!inMain(section)) continue;\n if (section.tab !== undefined) {\n throw say(\n `${where} names the tab \"${section.tab}\" in the main column. A thread is not one tab among others: leave out \"tab\", or give it to a section in region: 'aside'.`,\n );\n }\n if (section.kind === 'list' || section.kind === 'metric' || section.kind === 'chart' || section.kind === 'breakdown') {\n throw say(\n `${where} is ${named(section)} in the main column. ${fix}: ${section.kind === 'list' ? `related(${typeof section.title === 'string' ? `'${section.title}'` : '…'}, …, { region: 'aside' })` : \"{ region: 'aside' }\"}.`,\n );\n }\n }\n const after = each.find(({ section, index }) => index > thread.index && inMain(section));\n if (after) {\n throw say(\n `${after.where} (${named(after.section)}) stands after the timeline in the main column. ${fix}, or above the timeline, so the composer ends the thread.`,\n );\n }\n return;\n }\n\n if (layout === 'showcase') {\n const say = (text: string) => new Error(`[layout-showcase] ${text}`);\n const fix =\n \"A showcase opens with its photos: related('Photos', …, { presentation: 'gallery' }) first, its price headline in the aside\";\n if (page.aside === false)\n throw say(`${at} is a showcase that says aside: false. A showcase is its panel beside the photos: leave out \"layout\" instead.`);\n const first = each.find(({ section }) => inMain(section));\n if (!first || first.section.kind !== 'list' || first.section.presentation !== 'gallery') {\n throw say(\n `${first ? `${first.where} is ${named(first.section)}, first in the main column` : `${at} has nothing in its main column`}. ${fix}.`,\n );\n }\n const panel = each.filter(({ section }) => inAside(section));\n if (!panel.length) throw say(`${at} is a showcase with nothing in the aside. ${fix}: headline('…', { region: 'aside' }).`);\n if (panel.length > PANEL_MOST) {\n throw say(\n `${at} has ${panel.length} sections in the aside, and a showcase's panel holds 1 to ${PANEL_MOST} beside the photos: move the rest into the main column, where they stand under the photos at full width.`,\n );\n }\n const tabbed = panel.find(({ section }) => section.tab !== undefined);\n if (tabbed)\n throw say(\n `${tabbed.where} names the tab \"${tabbed.section.tab}\" in the aside. A showcase's panel stands beside the photos whole: leave out \"tab\".`,\n );\n // The panel is a price, not a property rail: the author's own fields say what a buyer reads.\n delete page.defaultAside;\n return;\n }\n\n // article\n const say = (text: string) => new Error(`[layout-article] ${text}`);\n if (page.aside === false) throw say(`${at} is an article that says aside: false. An article has no aside already: remove \"aside\".`);\n for (const { section, where } of each) {\n if (section.region === 'aside') {\n const call =\n section.kind === 'fields' && section.presentation === 'card' && typeof section.title === 'string'\n ? `card('${section.title}', …)`\n : named(section);\n throw say(\n `${where} (${named(section)}) stands in the aside. An article has no aside: move ${call} into the column with region: 'main', or leave out \"layout\".`,\n );\n }\n if (section.width === 'third' || section.width === 'two-thirds') {\n throw say(\n `${where} has width: '${section.width}'. An article is one column at a reading measure, where a third is too narrow for a value: use 'half' or leave \"width\" out.`,\n );\n }\n }\n delete page.defaultAside;\n}\n\n/** A refusal as a reason: its text without the code, which names a word the author did not write. */\nfunction refusal(page: PageDef, layout: PageLayoutName): string | undefined {\n // The rule's own mutations (a headline beside the thread, no default rail) land on the copy.\n const trial: PageDef = {\n ...page,\n layout,\n sections: page.sections.map((section) => ({ ...section }) as PageSection),\n };\n try {\n applyPageLayout(trial, 'the page');\n return undefined;\n } catch (error) {\n return (error as Error).message.replace(/^\\[[a-z-]+\\]\\s*/, '');\n }\n}\n\nconst quoted = (section: PageSection) => ('title' in section && typeof section.title === 'string' ? ` \"${section.title}\"` : '');\n\n/**\n * The layout a page's sections already make, where it names none. Each archetype is chosen by the section it is\n * named for — a showcase by the photos it opens with, a conversation by the timeline it ends with, an article by\n * the record's long text with nothing but its facts and notes around it — and only where applyPageLayout takes the\n * page as written, so a chosen layout is never a refusal. The first that holds wins: photos lead the page they open,\n * a thread ends one. Read from the declarations alone, never a row, so every record of a page has one shape from the\n * first frame. Anything else is the record's own skeleton. The why is what `mistri check --defaults` says.\n */\nexport function chooseLayout(page: PageDef): { layout: PageLayoutWord; why: string } {\n const main = page.sections.filter(inMain);\n const missed: string[] = [];\n const first = main[0];\n if (first?.kind === 'list' && first.presentation === 'gallery') {\n const no = refusal(page, 'showcase');\n if (!no)\n return {\n layout: 'showcase',\n why: `it opens with the gallery${quoted(first)}, with a panel beside it`,\n };\n missed.push(`it opens with a gallery, but a showcase would refuse it: ${no}`);\n } else missed.push('it opens with no gallery');\n const thread = main.find((section) => section.kind === 'timeline');\n if (thread) {\n const no = refusal(page, 'conversation');\n if (!no)\n return {\n layout: 'conversation',\n why: `its main column ends with the timeline${quoted(thread)}, and no list stands in it`,\n };\n missed.push(`it has a timeline, but a conversation would refuse it: ${no}`);\n } else missed.push('it has no timeline');\n const shown = page.sections.filter((section) => !section.hidden);\n const words = shown.every(\n (section) =>\n section.kind === 'note' || (section.kind === 'fields' && section.presentation !== 'card' && section.presentation !== 'headline'),\n );\n // The record's own long text, read in the column; a header's facts are a line under its name, never the words.\n const long = shown.flatMap((section) =>\n section.kind === 'fields' && section.presentation !== 'header'\n ? (section.paths ?? []).filter((path) => !path.includes('.') && isLongText(path))\n : [],\n );\n const facts = [...new Set(shown.flatMap((section) => (section.kind === 'fields' ? (section.paths ?? []) : [])))].filter(\n (path) => !long.includes(path),\n ).length;\n if (!words) missed.push('it holds more than fields and notes');\n else if (!long.length) missed.push('it names no long text, a description or a body');\n // As many facts as a rail holds: more is a record with a note on it, which keeps its rail.\n else if (facts > MAX_PROPERTIES) missed.push(`it has ${facts} facts beside its words, more than a rail's ${MAX_PROPERTIES}`);\n else {\n const no = refusal(page, 'article');\n if (!no)\n return {\n layout: 'article',\n why: `it is the record's words, ${long.map((path) => `\"${path}\"`).join(' and ')}, with ${facts} fact${facts === 1 ? '' : 's'} and nothing else`,\n };\n missed.push(`it is the record's words, but an article would refuse it: ${no}`);\n }\n return { layout: 'record', why: missed.join('; ') };\n}\n","/**\n * A record page's plan and every read its sections will ask, in one request\n * (shared/recordReads.ts). The reads are kept for the sections to find\n * (composedReads.ts); the plan is what the room draws from.\n *\n * A press on a row or a link to a record starts it (recordPress.ts), so\n * the request is on its way while the button is still down, and the room\n * that opens takes the one already running instead of asking again.\n */\nimport { api } from './core.js';\nimport { ownedBy } from './requestIdentity.js';\nimport { relatedRows } from './relatedRows.js';\nimport { composedEpoch, composing, holdComposed, type ComposedRead } from './composedReads.js';\nimport { sessionIdentity } from './sessionIdentity.js';\nimport { RECORD_COMPOSE_PATH } from '@mistriwork/engine/shared/recordReads.js';\nimport { canonicalText } from '@mistriwork/engine/shared/recordReadKeys.js';\nimport type { PageDef } from '@mistriwork/engine/shared/page.js';\nimport type { SectionPlan } from './components/pageModel.js';\n\ninterface Planned {\n page?: PageDef;\n plans?: SectionPlan[];\n}\n\n/* A compose begun by a press, until the room it opens takes it; a press\n that opened nothing lets it go. */\nconst begun = new Map<string, { work: Promise<Planned>; until: number; taken?: boolean }>();\nconst KEPT = 10_000;\n\nconst identityOf = (connection: string, page: string | PageDef, key: Record<string, unknown>, tab: string | undefined) =>\n canonicalText([connection, sessionIdentity().epoch, page, key, tab ?? null, relatedRows()]);\n\nfunction send(connection: string, page: string | PageDef, key: Record<string, unknown>, tab: string | undefined): Promise<Planned> {\n // Taken before the request leaves: a write sent or answered while it is out makes its answers the past.\n const since = composedEpoch();\n const work = (async (): Promise<Planned> => {\n let reply: { planned?: boolean; page?: PageDef; plans?: SectionPlan[]; reads?: ComposedRead[] } | undefined;\n try {\n reply = (await api(\n RECORD_COMPOSE_PATH as '/api/pages/plan',\n { ...ownedBy(connection), page, key, rows: relatedRows(), ...(tab === undefined ? {} : { tab }) } as never,\n )) as typeof reply;\n } catch {\n reply = undefined;\n }\n /* No plan came with it: the plan is asked alone, and says in its own words why it cannot be made. */\n if (!reply?.planned)\n return (await api('/api/pages/plan', { ...ownedBy(connection), page: page as never, key: key as never })) as Planned;\n holdComposed(connection, reply.reads ?? [], since);\n return { page: reply.page, plans: reply.plans };\n })();\n composing(connection, work);\n return work;\n}\n\n/** The page's plan, with its reads kept for the sections it draws on `tab`: the one a press began, or a new one. */\nexport function composeRecordPage(\n connection: string,\n page: string | PageDef,\n key: Record<string, unknown>,\n tab?: string,\n): Promise<Planned> {\n const id = identityOf(connection, page, key, tab);\n const early = begun.get(id);\n /* Taken once: the room's next read of its plan (a tab coming back) is a\n fresh one. Kept, taken, until it expires, so a press that lingers (its\n focus, its Enter) does not begin the page it just opened again. */\n if (early && !early.taken && early.until > Date.now()) {\n early.taken = true;\n return early.work;\n }\n return send(connection, page, key, tab);\n}\n\n/** Begin a record page's compose ahead of the navigation that will ask for it. */\nexport function beginRecordPage(connection: string, page: string, key: Record<string, unknown>): void {\n const id = identityOf(connection, page, key, undefined);\n const now = Date.now();\n for (const [kept, entry] of begun) if (entry.until <= now) begun.delete(kept);\n if (begun.has(id)) return;\n const work = send(connection, page, key, undefined);\n work.catch(() => {\n begun.delete(id);\n });\n begun.set(id, { work, until: now + KEPT });\n}\n","/** What recordSurface says, in the record page room (packages/kit/src/client/messages.ts says how the catalog is split). */\nimport { defineMessages } from '../../../messages.js';\n\nexport const say = defineMessages({\n thisRecordSurfaceIsNoLonger: 'This record surface is no longer current.',\n couldNotRefreshThisRecord: 'Could not refresh this record. Showing the last loaded layout.',\n incompleteSurfaceRoot: 'Incomplete surface root',\n thisSurfaceNeedsAComplete: 'This surface needs a complete record key and the page’s base table.',\n});\n","/** Shared page reads. A host owns presentation; this module never moves the walk,\n * changes a draft, settles a mutation or records a visit. */\nimport { ownedBy } from '../requestIdentity.js';\nimport { useMemo, useSyncExternalStore } from 'react';\nimport { useQuery } from '@tanstack/react-query';\nimport { api, findTable, labelPath, primaryKey, state, writeAllowed } from '../core.js';\nimport { shell } from '../shellMode.js';\nimport { grantedActionIds } from '../actionGrants.js';\nimport { connectionGeneration } from '../connectionTransition.js';\nimport { sessionIdentity, subscribeSessionIdentity } from '../sessionIdentity.js';\nimport { pageById, pagesFor } from '../page.js';\nimport { pathEqualityFilter, recordKey } from '../resourceRequests.js';\nimport type { TableMeta } from '@mistriwork/engine/shared/schema.js';\nimport type { PageDef } from '@mistriwork/engine/shared/page.js';\nimport type { PageProps } from './Page.js';\nimport { resourceOf } from './appRooms.js';\nimport { rootMatches, type PageEntry, type SectionPlan } from './pageModel.js';\nimport { useWalk } from './walk.js';\nimport { composeRecordPage } from '../recordCompose.js';\nimport { FOCUS_ALWAYS } from './mount.js';\nimport { say } from '../copy/record/components/recordSurface.js';\n\nexport type RecordSurfacePage = PageDef & { id: string; name: string; base: string; source: 'saved' | 'config' | 'suggested' };\nexport interface RecordSurfaceAlternative {\n id: string;\n name: string;\n source: string;\n}\nexport type RecordSurfaceEntry = PageEntry & { table: string; key: Record<string, unknown> };\n\n/** Capture once per host lifetime, not on every render. The host predicate must\n * fence close/reopen and replacement generations, including the same record.\n * It supplements the captured data authority; it cannot replace those checks. */\nconst capturedOwner = Symbol('record surface owner');\nexport interface RecordSurfaceOwner {\n readonly [capturedOwner]: true;\n readonly connection: string;\n readonly queryScope: number;\n readonly isCurrent: () => boolean;\n}\n\nlet nextScope = 0;\nexport function captureRecordSurfaceOwner(hostIsCurrent: () => boolean): RecordSurfaceOwner {\n const identity = sessionIdentity(),\n schema = state.schema;\n const connection = state.activeConnection ?? '',\n generation = connectionGeneration();\n const app = shell().app(),\n appMode = shell().inApp();\n return Object.freeze({\n [capturedOwner]: true as const,\n connection,\n queryScope: ++nextScope,\n isCurrent: () => {\n try {\n return (\n identity === sessionIdentity() &&\n identity.mode !== 'locked' &&\n identity.mode !== 'unknown' &&\n schema === state.schema &&\n connection === (state.activeConnection ?? '') &&\n generation === connectionGeneration() &&\n app === shell().app() &&\n appMode === shell().inApp() &&\n hostIsCurrent() === true\n );\n } catch {\n return false;\n }\n },\n });\n}\n\nclass RetiredSurface extends Error {\n constructor() {\n super(say('thisRecordSurfaceIsNoLonger'));\n }\n}\n\n/** Data for the existing PageView, including authored fixed actions and business\n * commands. Rendering a modal still requires host-aware PageView mutation and\n * dock integration: its current internal go() callbacks are route-specific. */\ntype RecordSurfaceContent = Pick<PageProps, 'entry' | 'page' | 'base' | 'plans' | 'others' | 'actions'>;\n\ntype RecordSurfaceState =\n | { status: 'retired' | 'loading' }\n | { status: 'error'; message: string }\n | { status: 'missing'; page: RecordSurfacePage; base: TableMeta }\n | { status: 'ready'; content: RecordSurfaceContent; refreshFailed: boolean };\n\nexport const RECORD_REFRESH_FAILED = say('couldNotRefreshThisRecord');\n\n/** Explicit non-route entry point. Never parses a URL, resolves a cold root or\n * falls back to the current route's entry. The host supplies label/tab changes\n * and consumes content only while this surface remains current. */\nexport function useRecordSurface({ owner, entry }: { owner: RecordSurfaceOwner; entry: RecordSurfaceEntry }) {\n const surface = usePageSurface(owner, entry, { explicitRoot: true });\n let result: RecordSurfaceState;\n if (!surface.isCurrent()) result = { status: 'retired' };\n else if (surface.definition.isError && !surface.page) result = { status: 'error', message: surface.definition.error.message };\n else if (!surface.page) result = { status: 'loading' };\n else if (surface.page.error) result = { status: 'error', message: surface.page.error };\n else if (!surface.base) result = { status: 'error', message: say('common.isNotATable', { base: surface.page.base }) };\n else if (surface.rootError) result = { status: 'error', message: surface.rootError };\n else if (surface.gone) result = { status: 'missing', page: surface.page, base: surface.base };\n else if (surface.plan.isError && !surface.content) result = { status: 'error', message: surface.plan.error.message };\n // Same-owner background errors cannot retire an editor or its unresolved\n // nested mutations. Cached data never crosses the ownership checks above.\n else if (surface.content) result = { status: 'ready', content: surface.content, refreshFailed: surface.refreshFailed };\n else result = { status: 'loading' };\n return { result, rootRow: surface.rootRow, isCurrent: surface.isCurrent };\n}\n\n/** Shared pipeline also used by PageRoom before its routed wrapper has resolved\n * a cold URL or chosen a root. Only that wrapper may mutate the routed entry. */\nexport function usePageSurface(owner: RecordSurfaceOwner, entry: PageEntry, options: { explicitRoot?: boolean } = {}) {\n useSyncExternalStore(subscribeSessionIdentity, sessionIdentity, sessionIdentity);\n useWalk();\n const keyText = JSON.stringify(entry.key);\n const target = useMemo(() => {\n const pageId = entry.pageId,\n table = entry.table,\n rootBody = entry.rootBody;\n const key = entry.key ? Object.freeze({ ...entry.key }) : undefined;\n const isCurrent = () =>\n owner[capturedOwner] === true &&\n owner.isCurrent() &&\n entry.pageId === pageId &&\n entry.table === table &&\n entry.rootBody === rootBody &&\n JSON.stringify(entry.key) === keyText;\n // Check after rejection as well as fulfillment: stale failures cannot become\n // a missing-record state or start another request under a replacement owner.\n const read = async <T>(request: () => Promise<T>): Promise<T> => {\n if (!isCurrent()) throw new RetiredSurface();\n try {\n const reply = await request();\n if (!isCurrent()) throw new RetiredSurface();\n return reply;\n } catch (error) {\n if (!isCurrent()) throw new RetiredSurface();\n throw error;\n }\n };\n return { pageId, table, key, read, isCurrent };\n }, [owner, entry, entry.pageId, entry.table, entry.rootBody, keyText]);\n const current = target.isCurrent();\n const definition = useQuery<RecordSurfacePage>({\n queryKey: ['page-def', owner.connection, target.pageId, target.table, owner.queryScope],\n queryFn: () => target.read(() => pageById(target.pageId, target.table) as Promise<RecordSurfacePage>),\n enabled: current,\n staleTime: 0,\n gcTime: 0,\n });\n const page = current ? definition.data : undefined;\n const base = page && !page.error ? (findTable(page.base) as TableMeta | undefined) : undefined;\n /* A suggested page is its table's own: the base and the id are in its name,\n so its root and its plan are asked beside its definition, not after it —\n one round trip off every record a page was never configured for. The\n content still waits for the definition. */\n const suggested =\n !page && target.pageId?.startsWith('suggested:')\n ? (findTable(target.pageId.slice('suggested:'.length)) as TableMeta | undefined)\n : undefined;\n const readBase = base ?? suggested;\n const readPageId = page?.id ?? (suggested ? target.pageId : undefined);\n const needsResolution = Boolean(readBase && (entry.table !== readBase.id || (!entry.key && entry.rootBody !== undefined)));\n let rootError: string | undefined;\n if (options.explicitRoot && readBase) {\n try {\n const checked = recordKey(target.key),\n pk = primaryKey(readBase) as string[];\n if (\n entry.table !== readBase.id ||\n pk.length !== Object.keys(checked).length ||\n pk.some((column) => !Object.hasOwn(checked, column))\n ) {\n throw new Error(say('incompleteSurfaceRoot'));\n }\n } catch {\n rootError = say('thisSurfaceNeedsAComplete');\n }\n }\n const others = useQuery<RecordSurfaceAlternative[]>({\n queryKey: ['pages-of', owner.connection, readBase?.id, owner.queryScope],\n queryFn: () =>\n target.read(async () => {\n try {\n return ((await pagesFor(readBase!.id)) as Array<RecordSurfaceAlternative & { error?: string }>).filter((p) => !p.error);\n } catch {\n return [];\n } // Alternatives failing never removes the known layout.\n }),\n enabled: current && Boolean(readBase) && !needsResolution && !rootError,\n staleTime: 0,\n gcTime: 0,\n });\n const plan = useQuery<{ page?: PageDef; plans?: SectionPlan[] }>({\n queryKey: ['page-plan', owner.connection, readPageId, keyText, page?.source === 'saved' ? JSON.stringify(page) : '', owner.queryScope],\n /* The plan and every read the page's sections will ask, in one request\n (shared/recordReads.ts), or the one a press on the row already began.\n Asked before the probe, which then waits for the row the compose reads. */\n queryFn: () =>\n target.read(() => composeRecordPage(owner.connection, page?.source === 'saved' ? page : readPageId!, target.key!, entry.tab)),\n /* Planning independently attests the root on the scoped server adapter.\n Start it beside the probe: the probe still decides whether this surface\n may render, but no longer puts a round trip ahead of every section. */\n enabled: current && !needsResolution && !rootError && Boolean(readBase && readPageId && target.key),\n staleTime: 0,\n gcTime: 0,\n // The record on screen: a tab coming back re-reads it, however recent.\n meta: FOCUS_ALWAYS,\n });\n const probe = useQuery<{ row: Record<string, unknown> | null; unreachable?: boolean }>({\n queryKey: ['page-root', owner.connection, readPageId, keyText, owner.queryScope],\n queryFn: () =>\n target.read(async () => {\n try {\n const namePath = labelPath(readBase!) as string | undefined;\n const pk = primaryKey(readBase!) as string[];\n const reply = await api('/api/view/run', {\n ...ownedBy(owner.connection),\n count: 'none',\n view: {\n id: `page:${readPageId!}:root`,\n name: readBase!.name,\n base: readBase!.id,\n columns: [...pk, ...(namePath ? [namePath] : [])].map((path) => ({ path })),\n filter: pathEqualityFilter(Object.fromEntries(pk.map((column) => [column, target.key![column]]))),\n limit: 1,\n },\n });\n return { row: reply.rows?.[0] ?? null };\n } catch {\n // Preserve the routed behavior: an unreachable probe lets the planner\n // explain the failure, rather than asserting that the record is gone.\n return { row: null, unreachable: true };\n }\n }),\n // The root probe needs the resolved page/base/key only. Alternatives feed\n // the chooser in the eventual plan, so making this read wait for them\n // serializes two independent requests and delays the missing-root fence.\n enabled: current && !needsResolution && !rootError && Boolean(readBase && readPageId && target.key),\n staleTime: 0,\n gcTime: 0,\n });\n const gone = Boolean(current && probe.data && !probe.data.row && !probe.data.unreachable);\n /* One page object per plan: PageView keys its label ownership on the page\n it was given, and a modal host re-renders on every label it publishes,\n so a fresh spread here would republish forever once an aside is in play. */\n const presented = useMemo(\n () => (page && plan.data ? { ...(plan.data.page ?? page), source: plan.data.page?.source ?? page.source } : undefined),\n [page, plan.data],\n );\n let content: RecordSurfaceContent | undefined;\n if (\n current &&\n !needsResolution &&\n !rootError &&\n base &&\n page &&\n others.data &&\n target.key &&\n probe.data &&\n !gone &&\n plan.data &&\n presented\n ) {\n /* A record page offers every one of its resource's actions, whichever list it was opened from. */\n const declared = shell().inApp() ? resourceOf(shell().app(), base.id)?.actions : undefined;\n const actions: PageProps['actions'] = declared?.length\n ? {\n list: declared,\n queryScope: owner.queryScope,\n matches: async (when) => {\n if (!target.isCurrent()) return false;\n const matches = await rootMatches(base, target.key!, when, owner.connection);\n return target.isCurrent() && matches;\n },\n canUpdate: writeAllowed(base.id),\n grantedCommands: shell().app()?.grantedCommands ?? [],\n grantedActions: grantedActionIds(shell().app(), base.id),\n }\n : undefined;\n content = { entry, page: presented, base, plans: plan.data.plans ?? [], others: others.data, actions };\n }\n return {\n definition,\n page,\n base,\n needsResolution,\n rootError,\n others,\n probe,\n plan,\n gone,\n content,\n refreshFailed: definition.isError || plan.isError,\n rootRow: current ? probe.data?.row : undefined,\n isCurrent: target.isCurrent,\n };\n}\n","/**\n * Custom components, as the App definition declares them: an author's own\n * React module, referenced by path and placed only through a declared kind —\n * a `custom` section on a page or a dashboard, a value renderer where a\n * display rule stands, an input where a form draws a field, a list view's\n * rows (`customList`) or a free-form page (`definePage` in `pages/*.page.tsx`).\n *\n * The definition stays data. `app.ts` is evaluated in a worker and crosses\n * back by structured clone, so a component can never be embedded as a\n * function: `component(id, { module })` is a name and a path, and the server\n * compiles the module beside the App (packages/mistri/src/server/app/appComponents.ts). What\n * the component reads is declared here too — the record's own fields and\n * named reads of rows — so it is checked, planned and authorized like every\n * built-in section, and the module only draws what it is handed.\n *\n * Author code is deployment code, trusted like `commands.ts`; the server\n * stays the only security boundary. A signed-out visitor may meet one where\n * the public grant already lets them read — a page, a list or a value over a\n * resource `auth.public` opens — and every read it makes is the public\n * grant's, refused on the server like any other; what a component declares\n * it reads there is held to that grant as the App loads, so a public page\n * never breaks for its visitors. The landing and public forms stay a closed\n * set of kinds that never runs markup: no component stands on either.\n */\nimport { allowKeys, isRecord } from './model.js';\n\nexport const COMPONENT_KINDS = ['section', 'value', 'input', 'list', 'page'] as const;\nexport type ComponentKind = (typeof COMPONENT_KINDS)[number];\nexport const COMPONENT_PROP_TYPES = ['number', 'text', 'boolean', 'choice'] as const;\n\n/** One declared prop: JSON-safe, with its default and its bounds, so a value is checked at load. */\nexport type ComponentPropDecl =\n | { type: 'number'; default?: number; min?: number; max?: number }\n | { type: 'text'; default?: string; max?: number }\n | { type: 'boolean'; default?: boolean }\n | { type: 'choice'; options: string[]; default?: string };\n\n/** What `component()` returns: a name, a path relative to the App directory, a kind and its props. */\nexport interface ComponentDecl {\n id: string;\n module: string;\n kind: ComponentKind;\n props?: Record<string, ComponentPropDecl>;\n}\n\n/** The key a component is referenced by: lowercase, the file names it is served under start with it. */\nexport const COMPONENT_ID = /^[a-z][a-z0-9-]{0,47}$/;\n/** What a component module may be: TypeScript or JavaScript, with or without JSX. */\nconst COMPONENT_EXTENSIONS = ['.tsx', '.ts', '.jsx', '.js'] as const;\n/**\n * How many named reads one custom section may declare. Its reads are asked together, one request each, so this\n * bounds the fan-out of a single paint (the server runs one active connection per process); the section draws one\n * skeleton until all are in, so more reads never shift the layout. Four was short of a real section (a list, its\n * neighbours and a few tables to look names up in); eight keeps a paint at a handful of queries.\n */\nexport const MAX_CUSTOM_READS = 8;\n/** The keys `component()` writes. */\nconst COMPONENT_KEYS = ['id', 'module', 'kind', 'props'] as const;\n/** How many columns a custom list presentation reads beside the keys. */\nconst MAX_CUSTOM_LIST_SHOW = 32;\n/**\n * How many aggregate reads (`figures`) one custom section may declare. Each is one breakdown, asked with the paint's\n * other breakdowns in a single request (breakdownBatch.ts), so the bound is on the work one section adds to it.\n */\nexport const MAX_CUSTOM_FIGURES = 8;\n/** The keys a declared read of rows takes, on a page and on a dashboard. */\nexport const PAGE_READ_KEYS = ['from', 'whose', 'query', 'show', 'sort', 'limit'] as const;\nexport const DASH_READ_KEYS = ['from', 'query', 'show', 'sort', 'limit', 'over'] as const;\n/** The keys a declared aggregate read takes, on a page and on a dashboard. */\nconst PAGE_TOTAL_KEYS = ['from', 'measure', 'by', 'whose', 'query', 'limit'] as const;\nconst DASH_TOTAL_KEYS = ['from', 'measure', 'by', 'query', 'over', 'limit'] as const;\n/** What a total measures: the breakdown grammar's aggregates over one column of the resource (count takes none). */\nconst TOTAL_FUNCTIONS = ['count', 'sum', 'avg', 'min', 'max'] as const;\ntype TotalFunction = (typeof TOTAL_FUNCTIONS)[number];\nconst TOTAL_MEASURE = /^(count|(?:sum|avg|min|max)\\s+[A-Za-z_][A-Za-z0-9_]*)$/;\n/** The most groups one total returns, and what it returns when it says none. */\nconst MAX_TOTAL_GROUPS = 500;\nconst DEFAULT_TOTAL_GROUPS = 100;\n\nconst PATH = /^[A-Za-z_][A-Za-z0-9_]*(\\.[A-Za-z_][A-Za-z0-9_]*)*$/;\nconst NAME = /^[A-Za-z][A-Za-z0-9_]{0,39}$/;\n\n/** Whether a value is a component declaration: what a display rule or a section names in place of its own words. */\nexport function isComponentDecl(value: unknown): value is Record<string, unknown> {\n return isRecord(value) && typeof value.module === 'string' && typeof value.id === 'string';\n}\n\n/**\n * A component declaration, checked in shape. The file itself — that it\n * exists, inside the App directory, and compiles — is the loader's to judge\n * (it knows the directory); this is what can be said of the data alone.\n */\nexport function parseComponentDecl(raw: unknown, at: string, expected?: ComponentKind): ComponentDecl {\n if (!isRecord(raw)) throw new Error(`${at} is not a component — declare it with component('id', { module: './widgets/Thing.tsx' }).`);\n allowKeys(raw, COMPONENT_KEYS, `${at} (a component)`);\n const id = raw.id;\n if (typeof id !== 'string' || !COMPONENT_ID.test(id)) {\n throw new Error(\n `${at} has a component id ${JSON.stringify(id)}: lowercase letters, digits and hyphens, starting with a letter, at most 48 characters.`,\n );\n }\n const module = raw.module;\n if (\n typeof module !== 'string' ||\n !module.startsWith('./') ||\n module.includes('\\\\') ||\n module.split('/').some((part) => part === '..' || part === '') ||\n !COMPONENT_EXTENSIONS.some((ext) => module.endsWith(ext))\n ) {\n throw new Error(\n `${at}: the component \"${id}\" names the module ${JSON.stringify(module)}. A module is a path inside the App directory, ` +\n `written from it — './widgets/Thing.tsx' — ending in ${COMPONENT_EXTENSIONS.join(', ')}.`,\n );\n }\n const kind = raw.kind === undefined ? 'section' : raw.kind;\n if (!(COMPONENT_KINDS as readonly unknown[]).includes(kind)) {\n throw new Error(\n `${at}: the component \"${id}\" has a kind of ${JSON.stringify(kind)}. Use ${COMPONENT_KINDS.map((k) => `\"${k}\"`).join(' or ')}.`,\n );\n }\n if (expected && kind !== expected) {\n throw new Error(\n `${at} names the component \"${id}\", ${KIND_WORDS[kind as ComponentKind]}. ${PLACED[expected]} takes a component of kind \"${expected}\": ` +\n `component('${id}', { module, kind: '${expected}' }).`,\n );\n }\n const props = raw.props === undefined ? undefined : parsePropDecls(raw.props, `${at}: the component \"${id}\"`);\n return { id, module, kind: kind as ComponentKind, ...(props ? { props } : {}) };\n}\n\n/** What each kind is, and where each is placed, for a refusal that says both. */\nconst KIND_WORDS: Record<ComponentKind, string> = {\n section: 'a section',\n value: 'a value renderer',\n input: 'an input',\n list: 'a list presentation',\n page: 'a free-form page',\n};\nconst PLACED: Record<ComponentKind, string> = {\n section: 'A custom section',\n value: 'A display rule',\n input: 'An input',\n list: 'customList()',\n page: 'A free-form page (pages/*.page.tsx)',\n};\n\nfunction parsePropDecls(raw: unknown, at: string): Record<string, ComponentPropDecl> {\n if (!isRecord(raw))\n throw new Error(`${at} has \"props\" that are not an object of prop.number(), prop.text(), prop.bool() or prop.choice().`);\n const out: Record<string, ComponentPropDecl> = {};\n for (const [name, entry] of Object.entries(raw)) {\n const here = `${at}, prop \"${name}\"`;\n if (!NAME.test(name)) throw new Error(`${here}: a prop's name is a word of letters, digits and underscores.`);\n if (!isRecord(entry) || !(COMPONENT_PROP_TYPES as readonly unknown[]).includes(entry.type)) {\n throw new Error(`${here} is not a prop declaration — write prop.number(), prop.text(), prop.bool() or prop.choice([...]).`);\n }\n const decl = entry as ComponentPropDecl & Record<string, unknown>;\n if (decl.type === 'number') {\n allowKeys(decl, ['type', 'default', 'min', 'max'], here);\n for (const key of ['min', 'max'] as const) {\n if (decl[key] !== undefined && (typeof decl[key] !== 'number' || !Number.isFinite(decl[key])))\n throw new Error(`${here} has a \"${key}\" that is not a number.`);\n }\n if (decl.min !== undefined && decl.max !== undefined && decl.min > decl.max) throw new Error(`${here} has a \"min\" above its \"max\".`);\n } else if (decl.type === 'text') {\n allowKeys(decl, ['type', 'default', 'max'], here);\n if (decl.max !== undefined && (!Number.isInteger(decl.max) || decl.max < 1))\n throw new Error(`${here} has a \"max\" that is not a positive whole number.`);\n } else if (decl.type === 'boolean') {\n allowKeys(decl, ['type', 'default'], here);\n } else {\n allowKeys(decl, ['type', 'options', 'default'], here);\n if (\n !Array.isArray(decl.options) ||\n !decl.options.length ||\n decl.options.some((option) => typeof option !== 'string' || !option.trim())\n ) {\n throw new Error(`${here} is a choice with no \"options\": name its values, prop.choice(['week', 'month']).`);\n }\n }\n /* A default is a value like any other: checked against its own declaration now,\n so a component never receives what its declaration says it cannot. */\n if (decl.default !== undefined) propValue(decl, decl.default, `${here}, its default`);\n out[name] = { ...decl } as ComponentPropDecl;\n }\n return out;\n}\n\nfunction propValue(decl: ComponentPropDecl, value: unknown, at: string): unknown {\n switch (decl.type) {\n case 'number':\n if (typeof value !== 'number' || !Number.isFinite(value)) throw new Error(`${at} is ${JSON.stringify(value)}, not a number.`);\n if (decl.min !== undefined && value < decl.min) throw new Error(`${at} is ${value}, below its min of ${decl.min}.`);\n if (decl.max !== undefined && value > decl.max) throw new Error(`${at} is ${value}, above its max of ${decl.max}.`);\n return value;\n case 'text':\n if (typeof value !== 'string') throw new Error(`${at} is ${JSON.stringify(value)}, not text.`);\n if (decl.max !== undefined && value.length > decl.max) throw new Error(`${at} is longer than its max of ${decl.max} characters.`);\n return value;\n case 'boolean':\n if (typeof value !== 'boolean') throw new Error(`${at} is ${JSON.stringify(value)}, not true or false.`);\n return value;\n case 'choice':\n if (typeof value !== 'string' || !decl.options.includes(value))\n throw new Error(`${at} is ${JSON.stringify(value)}; it is one of ${decl.options.map((o) => `\"${o}\"`).join(', ')}.`);\n return value;\n }\n}\n\n/**\n * The props a placement hands its component: each value checked against the\n * component's declaration and every default filled in, so the module reads\n * exactly the shape its types promise. A prop the declaration does not name is\n * refused, as an unknown key is everywhere else.\n */\nexport function componentPropValues(decl: ComponentDecl, raw: unknown, at: string): Record<string, unknown> {\n if (raw !== undefined && !isRecord(raw)) throw new Error(`${at} has \"props\" that are not an object.`);\n const given = (raw ?? {}) as Record<string, unknown>;\n const declared = decl.props ?? {};\n for (const name of Object.keys(given)) {\n if (!Object.hasOwn(declared, name)) {\n const names = Object.keys(declared);\n throw new Error(\n `${at} gives the prop \"${name}\", which the component \"${decl.id}\" does not declare. ${names.length ? `It declares: ${names.join(', ')}.` : 'It declares none.'}`,\n );\n }\n }\n const out: Record<string, unknown> = {};\n for (const [name, prop] of Object.entries(declared)) {\n const value = Object.hasOwn(given, name) ? given[name] : prop.default;\n if (value !== undefined) out[name] = propValue(prop, value, `${at}, prop \"${name}\"`);\n }\n return out;\n}\n\n/** One declared read of rows on a page: related to the record by `whose`, or the resource's own rows, narrowed. */\nexport interface PageCustomRead {\n from: string;\n /** Config `whose`: the path from `from` back to the page's record. Absent, the rows are not tied to it. */\n path?: string;\n filter?: string;\n columns: string[];\n /** Query-bar text, as authored; `orderBy` is what the planner runs. */\n sort?: string;\n orderBy?: Array<{ column: string; direction: 'asc' | 'desc' }>;\n limit?: number;\n}\n\n/** One declared read of rows on a dashboard: the rows section's words, the window's `over` included. */\nexport interface DashCustomRead {\n table: string;\n query?: string;\n show: string[];\n sort?: string;\n limit?: number;\n over?: string;\n /** Served, never authored: the query without its own window, as a rows section's. */\n windowBase?: string;\n}\n\nfunction readShape(raw: unknown, at: string, keys: readonly string[]): Record<string, unknown> {\n if (!isRecord(raw)) throw new Error(`${at} is not an object like { from: 'invoice', show: ['number', 'total'] }.`);\n allowKeys(raw, keys, at);\n if (typeof raw.from !== 'string' || !raw.from.trim()) throw new Error(`${at} has no \"from\": the resource its rows come from.`);\n if (!Array.isArray(raw.show) || !raw.show.length || raw.show.some((path) => typeof path !== 'string' || !PATH.test(path.trim()))) {\n throw new Error(`${at} has no \"show\": the columns the component reads, label paths welcome — ['number', 'customer_id.name'].`);\n }\n for (const key of ['query', 'sort', 'whose', 'over'] as const) {\n if (raw[key] !== undefined && (typeof raw[key] !== 'string' || !(raw[key] as string).trim()))\n throw new Error(`${at} has a \"${key}\" that is not a non-empty string.`);\n }\n if (raw.limit !== undefined && (typeof raw.limit !== 'number' || !Number.isInteger(raw.limit) || raw.limit < 1 || raw.limit > 500)) {\n throw new Error(`${at} has a \"limit\" that is not a whole number from 1 to 500.`);\n }\n return raw;\n}\n\nfunction reads<T>(\n raw: unknown,\n at: string,\n one: (entry: unknown, here: string) => T,\n { key = 'rows', most = MAX_CUSTOM_READS, noun = 'reads' }: { key?: string; most?: number; noun?: string } = {},\n): Record<string, T> | undefined {\n if (raw === undefined) return undefined;\n if (!isRecord(raw) || !Object.keys(raw).length)\n throw new Error(`${at} has \"${key}\" that are not an object of named ${noun}: { runs: { from, show } }.`);\n const names = Object.keys(raw);\n if (names.length > most) throw new Error(`${at} declares ${names.length} ${noun}; a custom section reads at most ${most}.`);\n return Object.fromEntries(\n names.map((name) => {\n if (!NAME.test(name))\n throw new Error(`${at}: a read's name is a word of letters, digits and underscores, like \"runs\"; \"${name}\" is not.`);\n return [name, one(raw[name], `${at}, ${key} \"${name}\"`)];\n }),\n );\n}\n\nexport function parsePageReads(raw: unknown, at: string): Record<string, PageCustomRead> | undefined {\n return reads(raw, at, (entry, here) => {\n const r = readShape(entry, here, PAGE_READ_KEYS);\n return {\n from: (r.from as string).trim(),\n ...(r.whose === undefined ? {} : { path: (r.whose as string).trim() }),\n ...(r.query === undefined ? {} : { filter: (r.query as string).trim() }),\n columns: (r.show as string[]).map((path) => path.trim()),\n ...(r.sort === undefined ? {} : { sort: (r.sort as string).trim() }),\n ...(r.limit === undefined ? {} : { limit: r.limit as number }),\n };\n });\n}\n\nexport function parseDashReads(raw: unknown, at: string): Record<string, DashCustomRead> | undefined {\n return reads(raw, at, (entry, here) => {\n const r = readShape(entry, here, DASH_READ_KEYS);\n return {\n table: (r.from as string).trim(),\n ...(r.query === undefined ? {} : { query: (r.query as string).trim() }),\n show: (r.show as string[]).map((path) => path.trim()),\n ...(r.sort === undefined ? {} : { sort: (r.sort as string).trim() }),\n ...(r.limit === undefined ? {} : { limit: r.limit as number }),\n ...(r.over === undefined ? {} : { over: (r.over as string).trim() }),\n };\n });\n}\n\n/** One declared aggregate read on a page: a count, sum, average, minimum or maximum, tied to the record by `whose`. */\nexport interface PageCustomTotal {\n from: string;\n fn: TotalFunction;\n /** The column measured; absent for a count. */\n column?: string;\n /** What the figure is grouped by, in the breakdown grammar (\"status\", \"month opened_on\"); absent, one figure. */\n by?: string;\n /** Config `whose`: the path from `from` back to the page's record. Absent, the resource's own rows. */\n path?: string;\n filter?: string;\n limit?: number;\n}\n\n/** One declared aggregate read on a dashboard: a measure in the breakdown grammar, narrowed by the window's `over`. */\nexport interface DashCustomTotal {\n table: string;\n /** \"count\" or \"sum amount\": the breakdown grammar's measure. */\n measure: string;\n by?: string;\n query?: string;\n over?: string;\n limit?: number;\n /** Served, never authored: the query without its own window, as a rows section's. */\n windowBase?: string;\n}\n\nfunction totalShape(raw: unknown, at: string, keys: readonly string[]): Record<string, unknown> & { measure: string } {\n if (!isRecord(raw)) throw new Error(`${at} is not an object like { from: 'invoice', measure: 'sum total' }.`);\n allowKeys(raw, keys, at);\n if (typeof raw.from !== 'string' || !raw.from.trim()) throw new Error(`${at} has no \"from\": the resource it measures.`);\n if (typeof raw.measure !== 'string' || !TOTAL_MEASURE.test(raw.measure.trim().replace(/\\s+/g, ' '))) {\n throw new Error(`${at} has no \"measure\": \"count\", or ${TOTAL_FUNCTIONS.slice(1).join(', ')} and a column — 'sum amount'.`);\n }\n for (const key of ['by', 'query', 'whose', 'over'] as const) {\n if (raw[key] !== undefined && (typeof raw[key] !== 'string' || !(raw[key] as string).trim()))\n throw new Error(`${at} has a \"${key}\" that is not a non-empty string.`);\n }\n if (raw.limit !== undefined) {\n if (raw.by === undefined) throw new Error(`${at} has a \"limit\" and no \"by\": a figure with no groups has no groups to cut.`);\n if (typeof raw.limit !== 'number' || !Number.isInteger(raw.limit) || raw.limit < 1 || raw.limit > MAX_TOTAL_GROUPS)\n throw new Error(`${at} has a \"limit\" that is not a whole number from 1 to ${MAX_TOTAL_GROUPS}.`);\n }\n return { ...raw, measure: raw.measure.trim().replace(/\\s+/g, ' ') };\n}\n\nconst totalReads = { key: 'figures', most: MAX_CUSTOM_FIGURES, noun: 'figures' };\n\nexport function parsePageTotals(raw: unknown, at: string): Record<string, PageCustomTotal> | undefined {\n return reads(\n raw,\n at,\n (entry, here) => {\n const r = totalShape(entry, here, PAGE_TOTAL_KEYS);\n const [fn, column] = r.measure.split(' ') as [TotalFunction, string | undefined];\n return {\n from: (r.from as string).trim(),\n fn,\n ...(column === undefined ? {} : { column }),\n ...(r.by === undefined ? {} : { by: (r.by as string).trim() }),\n ...(r.whose === undefined ? {} : { path: (r.whose as string).trim() }),\n ...(r.query === undefined ? {} : { filter: (r.query as string).trim() }),\n ...(r.limit === undefined ? {} : { limit: r.limit as number }),\n };\n },\n totalReads,\n );\n}\n\nexport function parseDashTotals(raw: unknown, at: string): Record<string, DashCustomTotal> | undefined {\n return reads(\n raw,\n at,\n (entry, here) => {\n const r = totalShape(entry, here, DASH_TOTAL_KEYS);\n return {\n table: (r.from as string).trim(),\n measure: r.measure,\n ...(r.by === undefined ? {} : { by: (r.by as string).trim() }),\n ...(r.query === undefined ? {} : { query: (r.query as string).trim() }),\n ...(r.over === undefined ? {} : { over: (r.over as string).trim() }),\n ...(r.limit === undefined ? {} : { limit: r.limit as number }),\n };\n },\n totalReads,\n );\n}\n\n/** How many groups a total with a `by` returns when it says no limit. */\nexport const groupsAsked = (limit: number | undefined): number => limit ?? DEFAULT_TOTAL_GROUPS;\n\n/** A record's own fields a page's custom section reads. */\nexport function parseRecordFields(raw: unknown, at: string): string[] | undefined {\n if (raw === undefined) return undefined;\n if (!Array.isArray(raw) || !raw.length || raw.some((path) => typeof path !== 'string' || !PATH.test(path.trim()))) {\n throw new Error(\n `${at} has a \"record\" that is not a list of the record's fields, label paths welcome — ['status', 'customer_id.name'].`,\n );\n }\n return raw.map((path) => (path as string).trim());\n}\n\n/** A custom list presentation as the App serves it: the component, the columns it reads, its props. */\nexport interface CustomListPresentation {\n kind: 'custom';\n component: ComponentDecl;\n show: string[];\n props: Record<string, unknown>;\n}\n\n/**\n * `customList(postStream, { show: ['body', 'author_id.display_name'] })`: a\n * list view's rows drawn by the author's component. The platform keeps the\n * query, the tabs, the filters, the sort, the paging and the bulk actions;\n * `show` is what the query reads beside the keys, so every row the component\n * receives carries exactly those columns.\n */\nexport function parseCustomList(raw: Record<string, unknown>, at: string): CustomListPresentation {\n allowKeys(raw, ['kind', 'component', 'show', 'props'], `${at} (a custom list)`);\n const component = parseComponentDecl(raw.component, `${at}, \"component\"`, 'list');\n const show = raw.show;\n if (\n !Array.isArray(show) ||\n !show.length ||\n show.length > MAX_CUSTOM_LIST_SHOW ||\n show.some((path) => typeof path !== 'string' || !PATH.test(path.trim()))\n ) {\n throw new Error(\n `${at} has no \"show\": the columns its component reads, one to ${MAX_CUSTOM_LIST_SHOW}, label paths welcome — ['body', 'author_id.display_name'].`,\n );\n }\n const columns = show.map((path) => (path as string).trim());\n if (new Set(columns).size !== columns.length) throw new Error(`${at} names one column twice in \"show\".`);\n return { kind: 'custom', component, show: columns, props: componentPropValues(component, raw.props, at) };\n}\n\n/** A free-form page as its `pages/*.page.tsx` file declares it, before it is served as the dashboard that draws it (appViews.ts). */\ninterface PageDecl {\n component: ComponentDecl;\n /** The resources the page reads, which gate it: a reader who cannot read one of them is not offered it. */\n reads: string[];\n /** The commands the page runs; any other is refused, in the browser and by the server. */\n commands: string[];\n /** The address parameters the page keeps its place in: `/{app}/pick-walk?list=12`. */\n params: string[];\n /** The loader that reads for it on the server, by its name in the commands module (`defineLoaders`). */\n loader?: string;\n props: Record<string, unknown>;\n}\n\n/** The most address parameters one page keeps. */\nconst MAX_PAGE_PARAMS = 8;\nconst PARAM = /^[a-z][a-z0-9_]{0,31}$/;\nconst LOADER = /^[A-Za-z][A-Za-z0-9_]{0,63}$/;\n\nconst words = (raw: unknown): raw is string[] => Array.isArray(raw) && raw.every((name) => typeof name === 'string' && name.trim() !== '');\n\n/**\n * `definePage({ reads: ['pick_list'], commands: ['pickList'], params: ['list'], component })`:\n * a whole page drawn by the author's component. What it reads and runs is\n * declared, so a reader who cannot read every one of its resources is not\n * offered it, `mistri check` holds the module to it, and authority.lock\n * records it; its place is kept in the address by the parameters it names,\n * so a page reached by link opens where it was. A bracketed code leads each\n * refusal, for `mistri check` (checkProblems.ts).\n */\nexport function parsePage(raw: Record<string, unknown>, at: string): PageDecl {\n const component = parseComponentDecl(raw.component, `${at}, \"component\"`, 'page');\n const reads = raw.reads;\n if (!words(reads) || !reads.length) {\n throw new Error(\n `[page-reads] ${at} has no \"reads\": the resources the page reads — reads: ['pick_list', 'order_line']. A page reads nothing it does not declare.`,\n );\n }\n const commands = raw.commands === undefined ? [] : raw.commands;\n if (!words(commands))\n throw new Error(`[page-commands] ${at} has \"commands\" that are not a list of command names: commands: ['pickList'].`);\n const params = raw.params === undefined ? [] : raw.params;\n if (\n !Array.isArray(params) ||\n params.length > MAX_PAGE_PARAMS ||\n params.some((name) => typeof name !== 'string' || !PARAM.test(name)) ||\n params.includes('period')\n ) {\n throw new Error(\n `[page-params] ${at} has \"params\" that are not up to ${MAX_PAGE_PARAMS} lowercase words, like params: ['list']; \"period\" is a dashboard's.`,\n );\n }\n if (raw.loader !== undefined && (typeof raw.loader !== 'string' || !LOADER.test(raw.loader))) {\n throw new Error(\n `[page-loader] ${at} has a \"loader\" that is not a loader's name, as the commands module's defineLoaders() registers it.`,\n );\n }\n return {\n component,\n reads: [...new Set(reads.map((name) => name.trim()))],\n commands: [...new Set(commands.map((name) => name.trim()))],\n params: [...new Set(params as string[])],\n ...(raw.loader === undefined ? {} : { loader: raw.loader as string }),\n props: componentPropValues(component, raw.props, at),\n };\n}\n\n/**\n * `inputs: { color: colorPicker }`: a component standing in a form's field —\n * a resource's column in its edit and New forms, or a command's input in an\n * action's or a signed-in form's command form. It draws the control and\n * hands back a value; what the value must be is still the form's rules and\n * the server's to judge.\n */\nexport function parseInputs(raw: unknown, at: string, options: { paths?: boolean } = {}): Record<string, ComponentDecl> | undefined {\n if (raw === undefined) return undefined;\n if (!isRecord(raw) || !Object.keys(raw).length)\n throw new Error(`${at} has \"inputs\" that are not an object of field → input component: { color: colorPicker }.`);\n /* A command's input inside a group is named by its path, `trade.vin`: in a repeating group, every row's. */\n const named = options.paths ? /^[A-Za-z_][A-Za-z0-9_]*(?:\\.[A-Za-z_][A-Za-z0-9_]*)*$/ : /^[A-Za-z_][A-Za-z0-9_]*$/;\n return Object.fromEntries(\n Object.entries(raw).map(([name, decl]) => {\n if (!named.test(name))\n throw new Error(`${at}, \"inputs\": \"${name}\" is not a field's name${options.paths ? ' or a path into a group (trade.vin)' : ''}.`);\n return [name, parseComponentDecl(decl, `${at}, inputs \"${name}\"`, 'input')];\n }),\n );\n}\n\n/** A command's input by its name or its path through groups (`trade.vin`), or undefined; a group itself is not one, nor are lines (their grid is the platform's). */\nexport function commandInputAt<F extends { name: string; type?: string; fields?: readonly F[] }>(\n fields: readonly F[],\n path: string,\n): F | undefined {\n const [head, ...rest] = path.split('.');\n const field = fields.find((candidate) => candidate.name === head);\n if (!field) return undefined;\n if (!rest.length) return field.type === 'group' || field.type === 'lines' ? undefined : field;\n return field.type === 'group' && field.fields ? commandInputAt(field.fields, rest.join('.')) : undefined;\n}\n","/*\n * Reading pages from configuration: the `pages` section of a tablewalk.json\n * and an App's page bodies. A malformed page throws with the page and, where\n * there is one, the fix; nothing is skipped.\n */\nimport { isMessageRef, parseCaption } from './appMessages.js';\nimport { parseTableSizing } from './tableSizing.js';\nimport { parseRelatedCreate } from './relatedCreate.js';\nimport { parseListEmpty } from './listEmpty.js';\nimport { CHART_SHAPES, FIGURE_FORMATS, FIGURE_FORMAT_WORDS, allowKeys, humanName } from './model.js';\nimport { headingOf } from './appNoun.js';\nimport { CARDS_MAX_LINES, parseCards, parseFacts, type AppCardsList } from './appCards.js';\nimport { parseIcon } from './appIcons.js';\nimport { slug } from './slug.js';\nimport { parsePageLayout } from './pageLayouts.js';\nimport { parseSortText } from './sortText.js';\nimport { componentPropValues, parseComponentDecl, parsePageReads, parsePageTotals, parseRecordFields } from './appComponents.js';\nimport { parseAppLinkAction, parseAppTarget, type AppLinkAction } from './appLinks.js';\nimport {\n type PageChart,\n type SectionWidth,\n WIDTHS,\n type SectionOffset,\n OFFSETS,\n type FieldColumns,\n type SectionRegion,\n SECTION_REGIONS,\n FIELD_COLUMNS,\n type PageMetric,\n type PageList,\n MAX_STREAMS,\n MAX_HISTORY_LIMIT,\n type TimelineComposer,\n type TimelineTone,\n type PageSection,\n type PageDef,\n MAX_SECTIONS,\n MAX_PAGE_SECTIONS,\n screenSections,\n type PageHeadlineAction,\n type PageSimilar,\n type PageGroup,\n GROUPS_MOST,\n GROUP_ROWS_MOST,\n} from './pageModel.js';\n\n/* ---------- config ---------- */\n\n/**\n * Read the `pages` section of a tablewalk.json.\n *\n * Throws on a malformed file rather than skipping the bad entry: a page that\n * silently did not appear is a bug report that starts \"I definitely wrote\n * it\". Everything a config file can get wrong is named with the page it is\n * in and, where there is one, the fix.\n */\nexport function parsePagesConfig(raw: unknown, where: string): PageDef[] {\n const pages = (raw as { pages?: unknown })?.pages;\n if (pages === undefined) return [];\n if (!Array.isArray(pages)) throw new Error(`${where}: \"pages\" must be an array.`);\n\n const seen = new Set<string>();\n return pages.map((entry, i) => {\n const at = `${where}: page ${i + 1}`;\n if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {\n throw new Error(`${at} is not an object.`);\n }\n const p = entry as Record<string, unknown>;\n const said = typeof p.label === 'string' && p.label.trim() ? ` (\"${p.label.trim()}\")` : '';\n if (p.id !== undefined && (typeof p.id !== 'string' || !p.id.trim())) {\n throw new Error(`${at}${said} has an \"id\" that is not a non-empty string.`);\n }\n const named = p.label !== undefined ? parseCaption(p.label, `${at}.label`) : typeof p.base === 'string' ? humanName(p.base.trim()) : '';\n const id = p.id === undefined ? `config-${slug(named, 'page')}` : String(p.id);\n const page = parsePageBody(p, at, id, true);\n /* An embed and a note's action name an App's views; a tablewalk.json has none to name. */\n for (const [n, section] of page.sections.entries()) {\n if (section.kind === 'list' && section.view !== undefined) {\n throw new Error(\n `${at} (\"${page.name}\") section ${n + 1} embeds the view \"${section.view}\", and a config file has no views: write a related list ({ kind: \"list\", from, whose }) instead.`,\n );\n }\n if (section.kind === 'note' && section.action) {\n throw new Error(\n `${at} (\"${page.name}\") section ${n + 1} is a note whose \"action\" needs an App, and a config file has no views to open. Remove \"action\".`,\n );\n }\n }\n if (seen.has(id)) throw new Error(`${at} (\"${page.name}\") repeats the id \"${id}\".`);\n seen.add(id);\n return page;\n });\n}\n\n/* Spellings renamed before 1.0, refused with the word that replaced them (allowKeys' `refused`). */\n/* A call to action's emphasis is its `intent`, as a button's is; `kind` names what a thing is. */\nconst CUSTOM_RENAMED = {\n totals: 'is now \"figures\": the named aggregate reads its component receives as figures.<name> (\"totals\" is a list\\'s footer of sums)',\n} as const;\nconst INTENT_RENAMED = { kind: 'is now \"intent\", as a button says it: \"primary\" or \"secondary\"' } as const;\nconst PAGE_RENAMED = { onlyOn: 'is now \"connection\", as the root of the config file says it' } as const;\n/** The keys a page takes. A tablewalk.json page also names its `id`; an App page is keyed by it. */\nexport const PAGE_KEYS = ['label', 'base', 'connection', 'listTabs', 'empty', 'sections', 'aside', 'layout'] as const;\n/** An App page has no `connection`: the App names its one connection, and a page scoped to another would never show. Nor a `label`: it is called its resource's `noun`. */\nexport const APP_PAGE_KEYS = PAGE_KEYS.filter((key) => key !== 'connection' && key !== 'label');\n/* `aside` and `layout` are an App page's alone: the Workbench's pages draw what they say, in the record's one skeleton. */\nconst WORKBENCH_PAGE_KEYS = PAGE_KEYS.filter((key) => key !== 'aside' && key !== 'layout');\nconst SECTION_COMMON = ['kind', 'title', 'region', 'tab', 'width', 'offsetLeft', 'offsetRight', 'hidden', 'when'] as const;\n/** A tab's name is a word or two across the strip, not a sentence. */\nconst MAX_TAB_NAME = 40;\n/** The keys each page section kind takes. Anything else is refused by name. A `segment` is read before the\n section is (appSegments.ts unfoldSegments), as its word in `query`. */\n/* `view` is an authored kind with no section of its own: `embed(view, { whose })`\n is read into a related list that remembers the view (PageList.view). */\nexport const PAGE_SECTION_KEYS: Record<PageSection['kind'] | 'view' | 'header' | 'current' | 'card' | 'headline', readonly string[]> = {\n fields: [...SECTION_COMMON, 'show', 'columns', 'names', 'presentation', 'editable'],\n /* The record a reference names, in a box of its own: the first shown field is its title, the rest its facts. */\n card: [...SECTION_COMMON, 'show'],\n /* A figure with its facts, its earlier value, its estimate and the calls to action beside it. */\n headline: [...SECTION_COMMON, 'show', 'links', 'subfigure', 'was', 'note'],\n /* The record's head: drawn where the head is, so nothing that would place it elsewhere. */\n header: ['kind', 'status', 'facts', 'primary', 'destructive', 'actions'],\n /* The one current related row as a card: nothing to page, sum or add under it. */\n current: ['kind', 'title', 'region', 'tab', 'width', 'when', 'from', 'whose', 'show', 'query', 'sort', 'empty'],\n timeline: [...SECTION_COMMON, 'streams', 'limit', 'create', 'newest', 'composer', 'tone', 'compact', 'side'],\n history: [...SECTION_COMMON, 'limit'],\n flows: [...SECTION_COMMON],\n diagram: [...SECTION_COMMON],\n custom: [...SECTION_COMMON, 'component', 'record', 'rows', 'figures', 'props'],\n metric: [...SECTION_COMMON, 'from', 'whose', 'about', 'measure', 'query', 'segment', 'format', 'unit'],\n breakdown: [...SECTION_COMMON, 'from', 'whose', 'about', 'by', 'measure', 'query', 'segment', 'limit', 'format', 'unit'],\n chart: [\n ...SECTION_COMMON,\n 'from',\n 'whose',\n 'about',\n 'by',\n 'measure',\n 'shape',\n 'sort',\n 'query',\n 'segment',\n 'limit',\n 'height',\n 'target',\n 'format',\n 'unit',\n ],\n note: [...SECTION_COMMON, 'text', 'icon', 'action', 'variant'],\n view: [...SECTION_COMMON, 'view', 'whose', 'about', 'limit'],\n list: [\n ...SECTION_COMMON,\n 'from',\n 'whose',\n 'show',\n 'query',\n 'segment',\n 'limit',\n 'sort',\n 'create',\n 'empty',\n 'sizing',\n 'presentation',\n 'totals',\n 'same',\n 'near',\n 'rowActions',\n 'actions',\n 'group',\n ],\n};\nexport const STREAM_KEYS = ['from', 'whose', 'at', 'show', 'detail', 'author', 'query', 'about', 'side', 'tone', 'compact'] as const;\n\n/**\n * One page body: a tablewalk.json page, whose `id` is its own or derives\n * from the name, or an App page, whose id is its key. The body is the same\n * either way, so the page builder, the scaffold and mistri check emit one\n * shape. Every key is checked: an unknown one is refused with the list.\n */\nexport function parsePageBody(\n p: Record<string, unknown>,\n at: string,\n id: string,\n withId = false,\n noun?: string,\n keys: readonly string[] = WORKBENCH_PAGE_KEYS,\n): PageDef {\n const caption = p.label === undefined ? undefined : parseCaption(p.label, `${at}.label`);\n if (typeof p.base !== 'string' || !p.base.trim()) {\n throw new Error(`${at}${typeof p.label === 'string' ? ` (\"${p.label.trim()}\")` : ''} has no \"base\" table.`);\n }\n /* A page is called what its records are: its resource's noun, else its table's name. */\n const name = caption ?? (noun ? headingOf(noun) : humanName(p.base.trim()));\n /* Said by name rather than as an unknown key: an author who copied an App's page into a tablewalk.json is told\n why it does not apply there, not that the word does not exist. */\n if (p.layout !== undefined && !keys.includes('layout')) {\n throw new Error(\n `[page-layout] ${at} (\"${name}\") has \"layout\", which is an App page's: a config file's page draws the record's one skeleton. Remove it.`,\n );\n }\n allowKeys(p, withId ? [...keys, 'id'] : keys, `${at} (\"${name}\")`, keys.includes('connection') ? PAGE_RENAMED : undefined);\n if (p.connection !== undefined && typeof p.connection !== 'string') {\n throw new Error(`${at} (\"${name}\") has a \"connection\" that is not a string.`);\n }\n /* Refused rather than coerced, like every other boolean in this file:\n `\"listTabs\": \"false\"` is a truthy string. */\n if (p.listTabs !== undefined && typeof p.listTabs !== 'boolean') {\n throw new Error(`${at} (\"${name}\") has \"listTabs\": ${JSON.stringify(p.listTabs)}. It must be true or false, without quotes.`);\n }\n /* The only value is the opt-out: a default aside is not something an author switches on. */\n if (p.aside !== undefined && p.aside !== false) {\n throw new Error(`${at} (\"${name}\") has \"aside\": ${JSON.stringify(p.aside)}. It can only be false, to leave the default aside out.`);\n }\n /* What every related list that says nothing of its own does while it has no rows. A list's own `empty` wins. */\n if (p.empty !== undefined && p.empty !== 'hide' && p.empty !== 'collapse') {\n throw new Error(\n `${at} (\"${name}\") has \"empty\": ${JSON.stringify(p.empty)}. A page's is 'hide' (leave an empty list out) or 'collapse' (say so in one line); a list takes guidance of its own.`,\n );\n }\n const pageEmpty = p.empty as 'hide' | 'collapse' | undefined;\n const sections = parseSections(p.sections, at, name, keys.includes('connection')).map((section) =>\n pageEmpty && section.kind === 'list' && section.presentation !== 'card' && !section.empty && !section.emptyMode\n ? { ...section, emptyMode: pageEmpty }\n : section,\n );\n return {\n id,\n name,\n base: p.base.trim(),\n sections,\n connection: p.connection as string | undefined,\n /* Stored as written: an App's page draws a tab strip of its lists only\n when it says `true`, and the Workbench's only unless it says `false`. */\n ...(typeof p.listTabs === 'boolean' ? { listTabs: p.listTabs } : {}),\n ...(pageEmpty ? { empty: pageEmpty } : {}),\n ...(p.aside === false ? { aside: false as const } : {}),\n ...(p.layout !== undefined ? { layout: parsePageLayout(p.layout, `${at} (\"${name}\")`) } : {}),\n source: 'config' as const,\n };\n}\n\nfunction parseSections(raw: unknown, at: string, name: string, config: boolean): PageSection[] {\n if (raw === undefined) return [{ kind: 'fields', width: 'full' }];\n if (!Array.isArray(raw)) throw new Error(`${at} (\"${name}\") has \"sections\" that are not an array.`);\n if (raw.length > MAX_PAGE_SECTIONS) {\n throw new Error(`${at} (\"${name}\") has more than ${MAX_PAGE_SECTIONS} sections, its tabs' included.`);\n }\n const sections = raw.map((entry, i) => parseSection(entry, `${at} (\"${name}\") section ${i + 1}`, config));\n /* A page is a screen, not a report: what one screen draws is counted, so a record that holds more says it in\n tabs, each a screen of its own (a major incident's comms log, bridge and review beside the ticket's work). */\n const drawn = screenSections(sections);\n if (drawn > MAX_SECTIONS) {\n throw new Error(\n `${at} (\"${name}\") has more than ${MAX_SECTIONS} sections on one screen (${drawn}). A page is a screen, not a report: ` +\n 'give some of them a \"tab\", and each tab is counted as a screen of its own.',\n );\n }\n /* A record has one head. */\n if (sections.filter((s) => s.kind === 'fields' && s.presentation === 'header').length > 1) {\n throw new Error(`${at} (\"${name}\") has two record headers. A record has one head: say every fact in one header({ … }).`);\n }\n /* A strip of one tab is a heading with a border round it: the name says\n \"these are tabs of each other\", which takes two. Each region has its own. */\n for (const region of SECTION_REGIONS) {\n const tabs = new Set(sections.filter((s) => (s.region ?? 'main') === region && s.tab).map((s) => s.tab));\n if (tabs.size === 1) {\n throw new Error(\n `${at} (\"${name}\") names one tab, \"${[...tabs][0]}\"${region === 'aside' ? ' in its side panel' : ''}. A tab strip needs two tabs or more: name another, or leave \"tab\" out.`,\n );\n }\n }\n return sections;\n}\n\n/**\n * A column count, refused rather than clamped.\n *\n * Clamping `\"columns\": 7` to 3 would render a layout nobody asked for and say\n * nothing about it; the file is the thing being edited, so the file is where\n * the mistake belongs.\n */\nfunction parseFieldColumns(value: unknown, at: string): FieldColumns | undefined {\n if (value === undefined || value === null) return undefined;\n const n = Number(value);\n if (!FIELD_COLUMNS.includes(n as FieldColumns)) {\n throw new Error(`${at} has \"columns\" of ${JSON.stringify(value)}. Use 1, 2 or 3.`);\n }\n return n as FieldColumns;\n}\n\n/** A boolean the file must spell as one — `\"true\"` is a truthy string. */\nfunction parseFlag(raw: unknown, at: string, key: string): boolean | undefined {\n if (raw === undefined) return undefined;\n if (typeof raw !== 'boolean') {\n throw new Error(`${at} has \"${key}\": ${JSON.stringify(raw)}. It must be true or false, without quotes.`);\n }\n return raw || undefined;\n}\n\n/* The spellings a section may not use any more, each mapped to the word\n that replaced it. Refused by name, in the parser's usual voice, so a page\n written against the pre-rename format fails with its fix rather than\n half-working. The renames (docs/apps.md, \"The respelling\"): `whose`\n says the direction `path` hid, `about` moves the subject the\n way `of` only implied, `show` and `sort` are the query\n language's own words. */\nconst RENAMED: Record<string, string> = {\n tint: '\"tone\", as a display rule says it: { kind: { internal: \\'warn\\' } } on the timeline, or one tone on a stream',\n order: \"\\\"newest\\\": 'first' (the default) or 'last', the way a conversation reads\",\n edit: '\"editable\", as a list view says it',\n path: '\"whose\" — the column on the section\\'s table that names this record, e.g. \"customer_id\" or \"invoice_id.customer_id\"',\n of: '\"about\" — the record this section is about, named by a reference column of the base, e.g. \"site_id\"',\n paths: '\"show\" — the fields to show, e.g. [\"name\", \"account_id.name\"]',\n orderBy: '\"sort\" — written like the query bar, e.g. \"due_on desc, name\"',\n};\n\nfunction refuseRenamed(s: Record<string, unknown>, at: string, keys: string[]): void {\n for (const key of keys) {\n if (s[key] !== undefined) throw new Error(`${at} has \"${key}\", which is now ${RENAMED[key]}.`);\n }\n}\n\n/* The forms of a fields section that are gone, and what each is now, in one place: they were refused one at a time,\n each in its own words, so an upgrade met them one load after another with no sight of the rest. Every refusal of\n one of them ends with this whole table. */\nconst RETIRED_FIELDS_FORMS: ReadonlyArray<readonly [string, string]> = [\n [\"fields(…, { presentation: 'card' })\", \"card(title, 'dealer_id', ['rating', 'city']): a section of its own\"],\n [\"fields(…, { presentation: 'headline' })\", \"headline('price', { was, subfigure, note, links }): a section of its own\"],\n [\"fields(…, { presentation: 'accordion' })\", \"gone: a group of fields is always open; use 'summary', 'facts', 'table' or 'properties'\"],\n ['fields(…, { collapsed })', 'gone with the accordion: leave it out'],\n ['fields(…, { actions })', 'headline(figure, { links })'],\n ['fields(…, { subfigure | was | note })', 'headline(figure, { subfigure | was | note })'],\n];\n/** The mapping as lines for a refusal's end, without a closing full stop. */\nconst retiredFieldsForms = (): string =>\n `Fields forms that are gone, and what each is now:\\n${RETIRED_FIELDS_FORMS.map(([was, now]) => ` ${was} → ${now}`).join('\\n')}`;\n\ntype FieldPresentation = 'summary' | 'headline' | 'facts' | 'table' | 'properties' | 'card';\n/* What `fields` itself says; a headline and a card are sections of their own, and read into fields on the way. */\nconst FIELD_PRESENTATIONS: readonly FieldPresentation[] = ['summary', 'facts', 'table', 'properties', 'headline', 'card'];\n/* A field name or a label path, as `show` takes them: \"price\", \"dealer_id.name\". */\nconst FIELD_PATH = /^[A-Za-z_][A-Za-z0-9_]*(\\.[A-Za-z_][A-Za-z0-9_]*)*$/;\n\n/**\n * `measure` is the dashboard's word — \"count\", \"sum amount\" — and the one a\n * page metric or chart says, read into the planner's fn and column. Absent\n * is a count.\n */\nfunction parseMeasure(raw: unknown, at: string): { fn: PageMetric['fn']; column?: string } {\n if (raw === undefined) return { fn: 'count' };\n const m = /^\\s*(count|sum|avg|min|max)(?:\\s+(?:distinct\\s+)?([A-Za-z_][\\w]*))?(?:\\s+as\\s+\\w+)?\\s*$/i.exec(String(raw));\n if (!m) throw new Error(`${at} has a \"measure\" of \"${String(raw)}\" — write it like \"count\", \"sum amount\" or \"avg total as mean\".`);\n const fn = m[1].toLowerCase() as PageMetric['fn'];\n if (fn !== 'count' && !m[2])\n throw new Error(`${at} has a \"measure\" of \"${String(raw).trim()}\" with no column to ${fn}: write it like \"${fn} amount\".`);\n return { fn, ...(m[2] === undefined ? {} : { column: m[2].trim() }) };\n}\n\nfunction parseSection(entry: unknown, at: string, config: boolean): PageSection {\n if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {\n throw new Error(`${at} is not an object.`);\n }\n let s = entry as Record<string, unknown>;\n let kind = s.kind === undefined ? 'fields' : String(s.kind);\n /* What a tablewalk.json page may say that an App's page may not: the options only the Workbench's page builder\n sets, which an App leaves out as it leaves out a section (a hidden one is simply not written). */\n if (!config) {\n for (const key of ['hidden', 'offsetLeft', 'names', 'sizing'] as const) {\n if (s[key] !== undefined)\n throw new Error(\n `${at} has \"${key}\", which only a config file page takes. ${\n key === 'hidden'\n ? 'An App leaves a section out instead of hiding it'\n : key === 'sizing'\n ? /* Nothing on a page's related list keeps its columns from crowding; said so, rather than a replacement invented. */\n 'An App page does not use it: a related table fits its pane. For a wide one, name fewer columns in \"show\", or give the table a list view of its own, where `sizing: \\'scroll\\'` is taken, and link to it'\n : 'An App page does not use it'\n }.`,\n );\n }\n }\n /* The record's head and the current card are kinds of their own as they\n are written, so a refusal says \"header\" and \"current\" as the author\n did; the page draws them as the fields and the list they are. */\n if (kind === 'header') {\n /* `factLabels` is the page model's reading of the facts (pageModel.ts),\n and its type is published, so an author finds it there and writes it.\n A fact's word has one spelling, in `facts` itself. */\n if (Object.hasOwn(s, 'factLabels'))\n throw new Error(\n `${at} has no \"factLabels\": a fact's word is said in \"facts\" itself, as { column: 'steps', after: 'step' }. It takes: ${PAGE_SECTION_KEYS.header.join(', ')}.`,\n );\n allowKeys(s, PAGE_SECTION_KEYS.header, at);\n return parseHeader(s, at);\n }\n const current = kind === 'current';\n if (current) {\n allowKeys(s, PAGE_SECTION_KEYS.current, at);\n s = { ...s, kind: 'list', presentation: 'card' };\n kind = 'list';\n } else if (kind === 'list' && s.presentation === 'card') {\n throw new Error(\n `${at}: a related list's presentation is \"gallery\" or a cards object; the one current row as a card is its own section, current(title, from, whose, { query, sort, show }).`,\n );\n }\n /* A card and a headline are written as the sections they are and drawn as the fields they show. */\n const written = kind;\n if (written === 'card' || written === 'headline') {\n allowKeys(\n s,\n PAGE_SECTION_KEYS[written],\n at,\n written === 'headline' ? { actions: 'is now \"links\": the calls to action beside the figure' } : {},\n );\n s = { ...s, kind: 'fields', presentation: written };\n kind = 'fields';\n }\n if (s.sizing !== undefined && kind !== 'list') throw new Error(`${at}: sizing belongs on a related list.`);\n if (s.region !== undefined && !SECTION_REGIONS.includes(s.region as SectionRegion)) {\n throw new Error(`${at} has an invalid \"region\". Use \"main\" or \"aside\".`);\n }\n const tab = s.tab === undefined ? undefined : parseCaption(s.tab, `${at}.tab`);\n if (tab && tab.length > MAX_TAB_NAME) {\n throw new Error(`${at} has a \"tab\" that is not a name of at most ${MAX_TAB_NAME} characters, like \"Details\".`);\n }\n /* Where a section is drawn: its region, and the tab of that region's strip. The sections that are about the\n record's surroundings rather than the record — a card of another record, the one current row, the change\n history — stand beside it unless the author puts them in the main column. */\n const region =\n (s.region as SectionRegion | undefined) ?? (written === 'card' || current || kind === 'history' ? ('aside' as const) : undefined);\n const placement = {\n ...(region === undefined ? {} : { region }),\n ...(tab === undefined ? {} : { tab }),\n };\n if (s.create !== undefined && kind !== 'list' && kind !== 'timeline')\n throw new Error(`${at}: create belongs on a related list or a timeline.`);\n if (s.presentation !== undefined && kind !== 'fields' && kind !== 'list')\n throw new Error(`${at}: presentation belongs on fields (summary, facts, table, properties) or a related list (gallery, cards).`);\n const width = parseWidth(s.width, at);\n const offsetLeft = parseOffset(s.offsetLeft, at, 'offsetLeft');\n const offsetRight = parseOffset(s.offsetRight, at, 'offsetRight');\n const title = s.title === undefined ? undefined : isMessageRef(s.title) ? parseCaption(s.title, `${at}.title`) : String(s.title);\n const hidden = parseFlag(s.hidden, at, 'hidden');\n if (s.when !== undefined && (typeof s.when !== 'string' || !s.when.trim())) {\n throw new Error(`${at} has a \"when\" that is not a condition like \"status = converted\".`);\n }\n const when = s.when === undefined ? undefined : (s.when as string).trim();\n\n if (kind === 'fields') {\n refuseRenamed(s, at, ['paths', 'edit']);\n if (s.presentation === 'header')\n throw new Error(`${at}: the record's head is a section of its own, header({ status, facts, primary }), not a fields presentation.`);\n if (written === 'fields' && (s.presentation === 'card' || s.presentation === 'headline'))\n throw new Error(\n `${at}: a ${s.presentation} is its own section, ${\n s.presentation === 'card' ? \"card(title, 'dealer_id', ['rating', 'city'])\" : \"headline('price', { was, subfigure, note, links })\"\n }, not a fields presentation. ${retiredFieldsForms()}.`,\n );\n const authored =\n written === 'card' || written === 'headline' ? Object.fromEntries(Object.entries(s).filter(([k]) => k !== 'presentation')) : s;\n allowKeys(authored, PAGE_SECTION_KEYS[written === 'card' || written === 'headline' ? written : 'fields'], at, {\n collapsed: `is gone with the accordion: a group of fields is always open. ${retiredFieldsForms()}`,\n ...(written === 'fields'\n ? {\n actions: `belongs on a headline(figure, { links }). ${retiredFieldsForms()}`,\n subfigure: `belongs on a headline(figure, { subfigure }). ${retiredFieldsForms()}`,\n was: `belongs on a headline(figure, { was }). ${retiredFieldsForms()}`,\n note: `belongs on a headline(figure, { note }). ${retiredFieldsForms()}`,\n }\n : {}),\n });\n if (s.show !== undefined && !Array.isArray(s.show)) {\n throw new Error(`${at} has a \"show\" that is not an array of field names.`);\n }\n if (s.presentation !== undefined && !FIELD_PRESENTATIONS.includes(s.presentation as FieldPresentation)) {\n throw new Error(\n `${at}: a fields presentation is \"summary\", \"facts\", \"table\" or \"properties\"${\n s.presentation === 'accordion' ? `; the accordion is gone: a group of fields is always open. ${retiredFieldsForms()}` : ''\n }.`,\n );\n }\n if (written === 'headline' && (!Array.isArray(s.show) || !s.show.length)) {\n throw new Error(`${at}: a headline shows its figure and its facts; name them in \"show\", the figure first.`);\n }\n if (s.presentation === 'facts' && (!Array.isArray(s.show) || !s.show.length)) {\n throw new Error(`${at}: a facts line says the values it names; name them in \"show\".`);\n }\n if (written === 'card' && (!Array.isArray(s.show) || !s.show.length)) {\n throw new Error(`${at}: a card is titled by its first field and says the rest as facts; name them in \"show\", the title first.`);\n }\n if (s.editable !== undefined && s.editable !== true)\n throw new Error(\n `${at} has \"editable\": ${JSON.stringify(s.editable)}. It is true, to edit the properties in place; leave it out otherwise.`,\n );\n if (s.editable === true && s.presentation !== undefined && !['summary', 'properties'].includes(s.presentation as string)) {\n throw new Error(\n `${at}: \"editable\" belongs on the fields a reader edits — a summary or a property rail; a ${String(s.presentation)} says values, it does not hold them.`,\n );\n }\n if (s.was !== undefined && (typeof s.was !== 'string' || !FIELD_PATH.test(s.was.trim()))) {\n throw new Error(`${at} has a \"was\" that is not a field name, like \"was_price\": the figure's earlier value.`);\n }\n if (s.subfigure !== undefined && (typeof s.subfigure !== 'string' || !FIELD_PATH.test(s.subfigure.trim()))) {\n throw new Error(`${at} has a \"subfigure\" that is not a field name or a label path like \"account_id.name\".`);\n }\n if (s.note !== undefined && (typeof s.note !== 'string' || !s.note.trim() || s.note.trim().length > 300)) {\n throw new Error(`${at} has a \"note\" that is not text of at most 300 characters.`);\n }\n /* A reference says the record it names by default in an App, so the\n one thing worth writing is the opt-out. */\n if (s.names !== undefined && s.names !== false) {\n throw new Error(\n `${at} has \"names\": ${JSON.stringify(s.names)}. A reference shows the record it names by default; say false to keep the raw id, or leave it out.`,\n );\n }\n return {\n kind: 'fields',\n ...placement,\n title,\n width,\n offsetLeft,\n offsetRight,\n hidden,\n ...(when ? { when } : {}),\n paths: (s.show as string[] | undefined)?.map(String),\n columns: parseFieldColumns(s.columns, at),\n names: s.names === false ? false : undefined,\n ...(s.presentation === undefined ? {} : { presentation: s.presentation as FieldPresentation }),\n ...(s.editable === true ? { edit: true as const } : {}),\n ...(s.links === undefined ? {} : { actions: parseHeadlineActions(s.links, at) }),\n ...(s.subfigure === undefined ? {} : { subfigure: (s.subfigure as string).trim() }),\n ...(s.was === undefined ? {} : { was: (s.was as string).trim() }),\n ...(s.note === undefined ? {} : { note: (s.note as string).trim() }),\n };\n }\n\n if (kind === 'timeline') {\n refuseRenamed(s, at, ['tint', 'order']);\n allowKeys(s, PAGE_SECTION_KEYS.timeline, at);\n if (!Array.isArray(s.streams) || !s.streams.length) {\n throw new Error(`${at} is a timeline and needs \"streams\" — the dated child rows it merges.`);\n }\n if (s.streams.length > MAX_STREAMS) {\n throw new Error(`${at} merges more than ${MAX_STREAMS} streams.`);\n }\n const streams = (s.streams as unknown[]).map((raw, i) => {\n const here = `${at}, stream ${i + 1}`;\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) throw new Error(`${here} is not an object.`);\n const t = raw as Record<string, unknown>;\n refuseRenamed(t, here, ['path', 'tint']);\n allowKeys(t, STREAM_KEYS, here);\n const from = typeof t.from === 'string' ? t.from.trim() : '';\n if (!from) throw new Error(`${here} has no \"from\" table.`);\n const path = typeof t.whose === 'string' ? t.whose.trim() : '';\n if (!path) throw new Error(`${here} has no \"whose\" — the column that leads back to this record.`);\n const atCol = typeof t.at === 'string' ? t.at.trim() : '';\n if (!atCol) throw new Error(`${here} has no \"at\" — the date column the stream is ordered by.`);\n const show = typeof t.show === 'string' ? t.show.trim() : '';\n if (!show) throw new Error(`${here} has no \"show\" — the line each row becomes.`);\n /* `query` is the one spelling; the internal stream keeps its `filter`.\n `author` says who wrote a row, where `by` groups one everywhere else;\n the internal stream keeps its `by`. */\n const filterRaw = t.query;\n /* `about` as a list's: the stream's rows lead to the row a reference of the record names (a deal's company),\n not to the record, so a timeline reaches its parent's other children. */\n if (t.about !== undefined && (typeof t.about !== 'string' || !t.about.trim() || t.about.includes('.')))\n throw new Error(`${here} has an \"about\" that is not a reference column of the page's table, e.g. \"company_id\".`);\n /* The stream's own far side, where its rows name the other party in their own words (`person_id`, not `author_id`). */\n if (t.side !== undefined && (typeof t.side !== 'string' || !t.side.trim()))\n throw new Error(`${here} has a \"side\" that is not a condition like \"person_id = record.caller_id\".`);\n /* The stream's own dress, where every row of it is one kind (time logged, a system event): no column to name. */\n if (t.tone !== undefined && !(TIMELINE_TONES as readonly unknown[]).includes(t.tone))\n throw new Error(`${here} has a \"tone\" that is not one of ${TIMELINE_TONES.join(', ')}: every row of this stream wears it.`);\n if (t.compact !== undefined && t.compact !== true)\n throw new Error(`${here} has a \"compact\" that is not true: every row of this stream is one line, or leave it out.`);\n return {\n ...(typeof t.about === 'string' ? { of: t.about.trim() } : {}),\n from,\n path,\n at: atCol,\n show,\n detail: t.detail === undefined ? undefined : String(t.detail).trim() || undefined,\n by: t.author === undefined ? undefined : String(t.author).trim() || undefined,\n filter: filterRaw === undefined ? undefined : String(filterRaw).trim() || undefined,\n ...(typeof t.side === 'string' ? { side: t.side.trim() } : {}),\n ...(t.tone === undefined ? {} : { tone: t.tone as TimelineTone }),\n ...(t.compact === true ? { compact: true as const } : {}),\n };\n });\n const limit = s.limit === undefined ? undefined : Number(s.limit);\n if (limit !== undefined && (!Number.isInteger(limit) || limit < 1)) {\n throw new Error(`${at} has a \"limit\" that is not a positive whole number.`);\n }\n if (s.newest !== undefined && s.newest !== 'first' && s.newest !== 'last') {\n throw new Error(`${at} has a \"newest\" that is neither 'first' (the default) nor 'last' (a conversation, read down to the latest).`);\n }\n const composer = s.composer === undefined ? undefined : parseTimelineComposer(s.composer, at);\n const tone = s.tone === undefined ? undefined : parseTimelineTone(s.tone, at);\n const compact = s.compact === undefined ? undefined : parseTimelineCompact(s.compact, at);\n /* The other party's rows: a condition in the query language over each stream's rows, where `record.<column>` is\n the page's record. Judged per stream against the catalog with the rest of the section (page.ts timelineSides). */\n if (s.side !== undefined && (typeof s.side !== 'string' || !s.side.trim())) {\n throw new Error(`${at} has a \"side\" that is not a condition like \"author_id = record.caller_id\".`);\n }\n const side = typeof s.side === 'string' ? s.side.trim() : undefined;\n if (composer && s.create !== undefined) {\n throw new Error(\n `${at} has both \"composer\" and \"create\": a conversation's composer is the commands' (reply, note), and \"create\" is the form's. Keep one.`,\n );\n }\n return {\n kind: 'timeline',\n ...placement,\n title,\n streams,\n limit,\n width,\n offsetLeft,\n offsetRight,\n hidden,\n ...(s.newest === 'last' ? { newest: 'last' as const } : {}),\n ...(composer ? { composer } : {}),\n ...(tone ? { tone } : {}),\n ...(compact ? { compact } : {}),\n ...(side ? { side } : {}),\n ...(s.create === undefined ? {} : { create: parseRelatedCreate(s.create, at) }),\n ...(when ? { when } : {}),\n };\n }\n\n if (kind === 'custom') {\n allowKeys(s, PAGE_SECTION_KEYS.custom, at, CUSTOM_RENAMED);\n /* The author's own component, placed like any section: its reads are\n declared, so the planner checks and runs them (appComponents.ts). */\n const component = parseComponentDecl(s.component, `${at}, \"component\"`, 'section');\n const parsed = parsePageReads(s.rows, at);\n const reads =\n parsed &&\n Object.fromEntries(\n Object.entries(parsed).map(([name, read]) => [\n name,\n read.sort === undefined ? read : { ...read, orderBy: parseSort(read.sort, `${at}, rows \"${name}\"`) },\n ]),\n );\n const totals = parsePageTotals(s.figures, at);\n return {\n kind: 'custom',\n ...placement,\n title,\n width,\n offsetLeft,\n offsetRight,\n hidden,\n ...(when ? { when } : {}),\n component,\n ...(s.record === undefined ? {} : { record: parseRecordFields(s.record, at) }),\n ...(reads ? { reads } : {}),\n ...(totals ? { aggregates: totals } : {}),\n props: componentPropValues(component, s.props, at),\n };\n }\n\n if (kind === 'history') {\n allowKeys(s, PAGE_SECTION_KEYS.history, at);\n const limit = s.limit === undefined ? undefined : Number(s.limit);\n if (limit !== undefined && (!Number.isInteger(limit) || limit < 1 || limit > MAX_HISTORY_LIMIT)) {\n throw new Error(`${at} has a \"limit\" that is not a whole number from 1 to ${MAX_HISTORY_LIMIT}.`);\n }\n return {\n kind: 'history',\n ...placement,\n title,\n width,\n offsetLeft,\n offsetRight,\n hidden,\n ...(limit === undefined ? {} : { limit }),\n ...(when ? { when } : {}),\n };\n }\n\n if (kind === 'flows' || kind === 'diagram') {\n allowKeys(s, PAGE_SECTION_KEYS[kind], at);\n return {\n kind,\n ...placement,\n title,\n width,\n offsetLeft,\n offsetRight,\n hidden,\n ...(when ? { when } : {}),\n };\n }\n\n if (kind === 'note') {\n allowKeys(s, PAGE_SECTION_KEYS.note, at);\n if (typeof s.text !== 'string' || !s.text.trim()) throw new Error(`${at} is a note and has no \"text\" — the words it says.`);\n let action: AppLinkAction | undefined;\n if (s.action !== undefined) {\n action = parseAppLinkAction(s.action, `${at}, \"action\"`, true);\n }\n if (s.variant !== undefined && s.variant !== 'quiet' && s.variant !== 'callout') {\n throw new Error(\n `${at} has a \"variant\" of ${JSON.stringify(s.variant)}: a note is \"quiet\" (small print, no mark) or a \"callout\" (a ground of its own); leave it out for the plain note.`,\n );\n }\n return {\n kind: 'note',\n ...placement,\n title,\n width,\n offsetLeft,\n offsetRight,\n hidden,\n ...(when ? { when } : {}),\n ...(s.variant === undefined ? {} : { variant: s.variant as 'quiet' | 'callout' }),\n text: s.text.trim(),\n ...(s.icon === undefined ? {} : { icon: parseIcon(s.icon, at) }),\n ...(action ? { action } : {}),\n };\n }\n\n if (kind === 'view') {\n allowKeys(s, PAGE_SECTION_KEYS.view, at);\n if (typeof s.view !== 'string' || !s.view.trim()) throw new Error(`${at} embeds no \"view\" — the key of one of the App's list views.`);\n const path = typeof s.whose === 'string' ? s.whose.trim() : '';\n if (!path) {\n throw new Error(\n `${at} embeds \"${s.view.trim()}\" with no \"whose\" — on a page, an embedded list shows the rows *whose* named column ` +\n \"is this record: embed('invoices', { whose: 'customer_id' }).\",\n );\n }\n if (s.about !== undefined && (typeof s.about !== 'string' || !s.about.trim())) {\n throw new Error(`${at} has an \"about\" that is not a non-empty string.`);\n }\n if (s.limit !== undefined && (typeof s.limit !== 'number' || !Number.isInteger(s.limit) || s.limit < 1)) {\n throw new Error(`${at} has a \"limit\" that is not a positive whole number.`);\n }\n return {\n kind: 'list',\n ...placement,\n title,\n width,\n offsetLeft,\n offsetRight,\n hidden,\n ...(when ? { when } : {}),\n view: s.view.trim(),\n from: '',\n path,\n ...(s.about === undefined ? {} : { of: (s.about as string).trim() }),\n ...(s.limit === undefined ? {} : { limit: s.limit as number }),\n };\n }\n\n if (kind !== 'metric' && kind !== 'list' && kind !== 'breakdown' && kind !== 'chart') {\n throw new Error(\n `${at} has an unknown kind \"${kind}\". Use \"fields\", \"metric\", \"list\", \"breakdown\", \"chart\", \"note\", \"timeline\", \"history\", \"flows\", \"diagram\", \"custom\" or \"view\" (an embedded list view); ` +\n 'in an App, each is written with its helper: fields(), metric(), related(), breakdown(), chart(), note(), timeline(), history(), flows(), diagram(), custom(), embed().',\n );\n }\n\n refuseRenamed(s, at, kind === 'list' ? ['path', 'of', 'orderBy'] : ['path', 'of', 'orderBy', 'edit']);\n if (kind === 'list' && s.columns !== undefined) {\n throw new Error(`${at} has \"columns\", which is now \"show\" — the columns the list shows, e.g. [\"due_on\", \"status\"].`);\n }\n allowKeys(\n s,\n PAGE_SECTION_KEYS[kind],\n at,\n kind === 'list'\n ? {\n about: 'a related list no longer takes: a row that shares a column with the record is found by { same: [column] }',\n similar: \"is now the list's own match: related(title, from, { same: [column], near: { column: 0.25 } })\",\n editable: 'is gone: the lines of a record are edited in its form (a form\\'s \"lines\")',\n order: 'is gone with the editable lines; a list sorts with \"sort\"',\n }\n : {},\n );\n const from = typeof s.from === 'string' ? s.from.trim() : '';\n if (!from) throw new Error(`${at} has no \"from\" table.`);\n const similar = kind === 'list' && s.same !== undefined ? parseMatch(s, at) : undefined;\n if (kind === 'list' && s.near !== undefined && s.same === undefined)\n throw new Error(`${at} has \"near\" without \"same\": a match is the columns a row shares with the record, and the numbers it keeps near.`);\n const path = similar ? '' : typeof s.whose === 'string' ? s.whose.trim() : '';\n if (!path && !similar) {\n throw new Error(\n `${at} has no \"whose\" — the rows shown are the ${from} rows *whose* named column ` +\n 'is this record: \"customer_id\" for the customer\\'s own rows, ' +\n '\"invoice_id.customer_id\" for rows two references away; or, to list the rows that share its values, \"same\".',\n );\n }\n /* `query` is the one word for narrowing rows, on a page section as on a\n dashboard's; the internal section keeps its `filter`. */\n const filter = s.query === undefined ? undefined : String(s.query).trim() || undefined;\n /* `about` moves a figure's subject one reference away from the\n root: \"tickets at this order's site\" is { from: \"ticket\", whose:\n \"site_id\", about: \"site_id\" }. Absent means the root itself. */\n if (s.about !== undefined && (typeof s.about !== 'string' || !s.about.trim())) {\n throw new Error(`${at} has an \"about\" that is not a non-empty string.`);\n }\n const of = s.about === undefined ? undefined : (s.about as string).trim();\n\n /* A figure said as a dashboard's is (appViews.ts): an average of a 1–5 rating has no column rule to say it, and\n drew \"3.333\" with nothing after it; a win rate `avg won * 100` is a percent (G111). */\n if (s.format !== undefined && !(FIGURE_FORMATS as readonly string[]).includes(String(s.format)))\n throw new Error(`${at} has a \"format\" of \"${String(s.format)}\". Use ${FIGURE_FORMAT_WORDS}.`);\n if (s.unit !== undefined && (typeof s.unit !== 'string' || !s.unit.trim() || s.unit.length > 24))\n throw new Error(`${at} has a \"unit\" that is not a short word said after the figure, like \"days\".`);\n const figureWords = {\n ...(s.format === undefined ? {} : { format: s.format as PageMetric['format'] }),\n ...(s.unit === undefined ? {} : { unit: (s.unit as string).trim() }),\n };\n\n if (kind === 'metric') {\n const { fn, column: columnRaw } = parseMeasure(s.measure, at);\n return {\n kind: 'metric',\n ...placement,\n title,\n width,\n offsetLeft,\n offsetRight,\n hidden,\n ...(when ? { when } : {}),\n fn: fn as PageMetric['fn'],\n from,\n path,\n of,\n column: columnRaw === undefined ? undefined : String(columnRaw).trim(),\n filter,\n ...figureWords,\n };\n }\n\n if (kind === 'chart') {\n const by = typeof s.by === 'string' ? s.by.trim() : '';\n if (!by) throw new Error(`${at} has no \"by\" — what its groups are: \"status\", or a bucketed date like \"month issued_on\".`);\n if (s.shape !== undefined && !(CHART_SHAPES as readonly string[]).includes(String(s.shape))) {\n throw new Error(`${at} has a \"shape\" of \"${String(s.shape)}\". Use \"line\", \"bar\", \"donut\" or \"stacked\".`);\n }\n if (s.shape === 'stacked' && !by.includes(',')) {\n throw new Error(\n `${at} is a stacked chart and needs a two-key \"by\" — \"month issued_on, status\": a bar for each month, divided by status.`,\n );\n }\n if (s.limit !== undefined && (typeof s.limit !== 'number' || !Number.isInteger(s.limit) || s.limit < 1)) {\n throw new Error(`${at} has a \"limit\" that is not a positive whole number.`);\n }\n if (s.height !== undefined && (typeof s.height !== 'number' || !Number.isFinite(s.height) || s.height < 120)) {\n throw new Error(`${at} has a \"height\" that is not a number of pixels (120 or more).`);\n }\n if (s.target !== undefined && (typeof s.target !== 'number' || !Number.isFinite(s.target))) {\n throw new Error(`${at} has a \"target\" that is not a number.`);\n }\n if (s.target !== undefined && s.shape === 'donut')\n throw new Error(`${at} is a donut, which has no scale to draw a \"target\" on. Use a line or stacked bars.`);\n let sort: string | undefined;\n if (s.sort !== undefined) {\n if (s.shape !== 'bar' && s.shape !== 'donut')\n throw new Error(`${at} has a \"sort\", which orders the categories of a bar chart or a donut; a line runs along its key.`);\n if (s.sort === 'value' || s.sort === 'category') sort = s.sort;\n else {\n const parsed = parseSortText(s.sort, at, 'starts_on');\n if (parsed!.length !== 1 || parsed![0].column.includes('.'))\n throw new Error(\n `${at} has a \"sort\" of more than one column or a path; categories are ordered by 'value', 'category' or one column of the rows: \"starts_on desc\".`,\n );\n sort = parsed![0].direction === 'desc' ? `${parsed![0].column} desc` : parsed![0].column;\n }\n }\n const { fn, column } = parseMeasure(s.measure, at);\n return {\n kind: 'chart',\n ...placement,\n title,\n width,\n offsetLeft,\n offsetRight,\n hidden,\n ...(when ? { when } : {}),\n from,\n path,\n of,\n by,\n fn,\n ...(column === undefined ? {} : { column }),\n ...(s.shape === undefined ? {} : { shape: s.shape as PageChart['shape'] }),\n ...(sort ? { sort } : {}),\n filter,\n ...(s.limit === undefined ? {} : { limit: s.limit as number }),\n ...(s.height === undefined ? {} : { height: s.height as number }),\n ...(s.target === undefined ? {} : { target: s.target as number }),\n ...figureWords,\n };\n }\n\n if (kind === 'breakdown') {\n const by = typeof s.by === 'string' ? s.by.trim() : '';\n if (!by) {\n throw new Error(`${at} has no \"by\" — the column its groups are counted under.`);\n }\n if (s.limit !== undefined && (typeof s.limit !== 'number' || !Number.isInteger(s.limit) || s.limit < 1)) {\n throw new Error(`${at} has a \"limit\" that is not a positive whole number.`);\n }\n const measure = parseMeasure(s.measure, at);\n return {\n kind: 'breakdown',\n // A count is what a breakdown says when it says nothing.\n ...(measure.fn === 'count' && measure.column === undefined\n ? {}\n : { fn: measure.fn, ...(measure.column === undefined ? {} : { column: measure.column }) }),\n ...placement,\n title,\n width,\n offsetLeft,\n offsetRight,\n hidden,\n ...(when ? { when } : {}),\n from,\n path,\n of,\n by,\n filter,\n limit: s.limit as number | undefined,\n ...figureWords,\n };\n }\n\n if (s.limit !== undefined && (typeof s.limit !== 'number' || !Number.isInteger(s.limit) || s.limit < 1)) {\n throw new Error(`${at} has a \"limit\" that is not a positive whole number.`);\n }\n if (s.show !== undefined && !Array.isArray(s.show)) {\n throw new Error(`${at} has a \"show\" that is not an array of column names.`);\n }\n const shown = s.show === undefined ? undefined : parseShowEntries(s.show as unknown[], at);\n /* A gallery draws a related list of image files; whether the rows are\n files is the App's to say (`file` on the resource), judged where the\n page meets the App. */\n /* Or its rows as cards, in the list view's own cards grammar: the same\n keys, judged against the catalog where the page meets the App. */\n let cards: AppCardsList | undefined;\n /* The one current row as a card — the active lease, the latest visit —\n chosen by the list's query and sort: one row, so nothing to page, sum\n or add under it. */\n if (current && (s.empty === false || s.empty === 'hide' || s.empty === 'collapse'))\n throw new Error(`${at} is a current() card; say what no row means with \"empty\" guidance rather than leaving it out.`);\n if (s.presentation !== undefined && s.presentation !== 'gallery' && !current) {\n const raw = s.presentation;\n if (!raw || typeof raw !== 'object' || Array.isArray(raw) || (raw as Record<string, unknown>).kind !== 'cards') {\n throw new Error(`${at}: a related list's presentation is \"gallery\" or a cards object { kind: 'cards', primary, … }.`);\n }\n const c = raw as Record<string, unknown>;\n allowKeys(\n c,\n ['kind', 'primary', 'description', 'value', 'badge', 'icon', 'properties', 'facts', 'footer', 'image', 'media'],\n `${at}, presentation (cards)`,\n { lines: 'is now \"properties\": the captioned values under the title (\"facts\" stays the dotted line)' },\n );\n if (typeof c.primary !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*(\\.[A-Za-z_][A-Za-z0-9_]*)?$/.test(c.primary.trim()))\n throw new Error(`${at}, presentation (cards): \"primary\" must be a local column or a one-hop path like \"vehicle_id.title\".`);\n if (\n c.properties !== undefined &&\n (!Array.isArray(c.properties) ||\n c.properties.length > CARDS_MAX_LINES ||\n c.properties.some((line) => typeof line !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*(\\.[A-Za-z_][A-Za-z0-9_]*)?$/.test(line.trim())))\n ) {\n throw new Error(`${at}, presentation (cards): \"properties\" must be at most ${CARDS_MAX_LINES} local columns or one-hop label paths.`);\n }\n cards = parseCards(\n c,\n `${at}, presentation (cards)`,\n c.primary.trim(),\n ((c.properties as string[] | undefined) ?? []).map((line) => line.trim()),\n );\n }\n /* `empty` is the guidance a list with no rows shows, or what to do with the list instead: 'hide' leaves it out\n until it has a row (a manager's direct reports, on a page most people's are none), 'collapse' says it in one\n quiet line with the other empty lists beside it. Where the reader may add a row, 'hide' collapses rather than\n hides: a list that offers a door is never taken away (PageSections.tsx). */\n if (s.empty === true)\n throw new Error(\n `${at} has \"empty: true\". \"empty\" is guidance ({ title, description, action }), 'hide' to leave the list out while it has no rows, or 'collapse' to say so in one line.`,\n );\n if (s.empty === false) throw new Error(`${at} has \"empty: false\", which is now \"empty: 'hide'\".`);\n const emptyMode = s.empty === 'hide' || s.empty === 'collapse' ? s.empty : undefined;\n /* A footer of sums, the list view's `totals`: over every row that points\n at the record, not the page, so it is one aggregate over the rows the\n path reaches and never through a filter. */\n let totals: string[] | undefined;\n if (s.totals !== undefined) {\n if (\n !Array.isArray(s.totals) ||\n !s.totals.length ||\n s.totals.some((c) => typeof c !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(c))\n ) {\n throw new Error(`${at} has \"totals\" that are not a list of the rows' numeric columns, rollups or computed numbers.`);\n }\n if (new Set(s.totals).size !== s.totals.length) throw new Error(`${at} has \"totals\" that name one column twice.`);\n if (s.presentation !== undefined)\n throw new Error(\n `${at} has \"totals\" and a ${s.presentation === 'gallery' ? 'gallery' : 'cards'} presentation; a footer of totals belongs to a table.`,\n );\n const narrowed = ['query', 'near'].filter((key) => s[key] !== undefined && s[key] !== false);\n if (narrowed.length) {\n throw new Error(\n `${at} has \"totals\" and \"${narrowed[0]}\". Totals sum every row that points at the record and cannot be narrowed; drop one, or add a total() metric beside the list.`,\n );\n }\n totals = s.totals as string[];\n }\n const group = parseGroup(s, at, { similar: Boolean(similar), presentation: s.presentation, current });\n /* The child's own verbs, from a ⋯ at the end of each row, as its own page offers them: a table's, never a card's or a photo's. */\n if (s.rowActions !== undefined && s.rowActions !== true && s.rowActions !== false)\n throw new Error(`${at}: \"rowActions\" must be true (a ⋯ menu on each row with the row's own actions) or false.`);\n if (s.rowActions === true && s.presentation !== undefined)\n throw new Error(`${at} has \"rowActions\" and a presentation; a row menu belongs to a table.`);\n /* The record's own verbs that add to this list — \"Add to the committee\" on a deal — drawn in the list's head\n rather than the record's, by the ids its resource's actions have (checked against them where the App is). */\n let listActions: string[] | undefined;\n if (s.actions !== undefined) {\n if (\n !Array.isArray(s.actions) ||\n !s.actions.length ||\n s.actions.length > 4 ||\n s.actions.some((id) => typeof id !== 'string' || !id.trim())\n )\n throw new Error(`${at}: \"actions\" is one to four of the record's own action ids, drawn in the list's head: ['addBuyer'].`);\n listActions = (s.actions as string[]).map((id) => id.trim());\n if (new Set(listActions).size !== listActions.length) throw new Error(`${at} has \"actions\" naming one action twice.`);\n }\n return {\n kind: 'list',\n ...placement,\n ...(s.sizing === undefined ? {} : { sizing: parseTableSizing(s.sizing, at) }),\n ...(s.presentation === 'gallery' || s.presentation === 'card'\n ? { presentation: s.presentation }\n : cards\n ? { presentation: cards }\n : {}),\n ...(similar ? { similar } : {}),\n title,\n width,\n offsetLeft,\n offsetRight,\n hidden,\n ...(when ? { when } : {}),\n from,\n path,\n columns: shown?.columns,\n ...(shown?.labels ? { labels: shown.labels } : {}),\n filter,\n limit: s.presentation === 'card' ? 1 : (s.limit as number | undefined),\n orderBy: parseSort(s.sort, at),\n ...(s.create === undefined ? {} : s.create === false ? { noCreate: true as const } : { create: parseRelatedCreate(s.create, at) }),\n ...(s.empty === undefined || emptyMode ? {} : { empty: parseListEmpty(s.empty, at) }),\n ...(emptyMode ? { emptyMode } : {}),\n ...(group ? { group } : {}),\n ...(totals ? { totals } : {}),\n ...(s.rowActions === true ? { rowActions: true as const } : {}),\n ...(listActions ? { listActions } : {}),\n };\n}\n\n/**\n * A related list's `group`: one header for each record its rows share through `by`, the rows to a group under it.\n * One level only. `limit` beside it is the rows to a group, so it is held to what a group opens to.\n */\nfunction parseGroup(\n s: Record<string, unknown>,\n at: string,\n beside: { similar: boolean; presentation: unknown; current: boolean },\n): PageGroup | undefined {\n if (s.group === undefined) return undefined;\n const raw = s.group;\n if (!raw || typeof raw !== 'object' || Array.isArray(raw))\n throw new Error(\n `${at} has a \"group\" that is not an object: group: { by: 'estimate_id', show: ['vehicle_id', 'total'], sort: 'created_on desc', limit: 5 }.`,\n );\n if (beside.current) throw new Error(`${at} is a current() card, which is one row and has nothing to group.`);\n if (beside.similar)\n throw new Error(`${at} has \"group\" and \"same\": a matched list is not the record's children, so it has no chain worth grouping.`);\n if (beside.presentation !== undefined)\n throw new Error(\n `${at} has \"group\" and a ${beside.presentation === 'gallery' ? 'gallery' : 'cards'} presentation; groups are drawn as tables.`,\n );\n const g = raw as Record<string, unknown>;\n allowKeys(g, ['by', 'show', 'sort', 'limit', 'empty', 'sum'], `${at}, group`);\n if (typeof g.by !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*(\\.[A-Za-z_][A-Za-z0-9_]*)*$/.test(g.by.trim()))\n throw new Error(`${at}, group has no \"by\": the column or reference the rows are grouped under, like \"estimate_id\".`);\n if (g.show !== undefined) {\n if (\n !Array.isArray(g.show) ||\n !g.show.length ||\n g.show.length > 4 ||\n g.show.some((c) => typeof c !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(c))\n )\n throw new Error(`${at}, group has a \"show\" that is not one to four columns of the record each group is: ['vehicle_id', 'total'].`);\n if (new Set(g.show).size !== g.show.length) throw new Error(`${at}, group has a \"show\" that names one column twice.`);\n }\n let sort: PageGroup['sort'];\n if (g.sort !== undefined) {\n const parsed = parseSortText(g.sort, `${at}, group`, 'created_on desc')!;\n if (parsed.length !== 1 || parsed[0].column.includes('.'))\n throw new Error(\n `${at}, group has a \"sort\" of more than one column or a path; groups are ordered by one column of their record: \"created_on desc\".`,\n );\n sort = { column: parsed[0].column, direction: parsed[0].direction };\n }\n if (g.limit !== undefined && (typeof g.limit !== 'number' || !Number.isInteger(g.limit) || g.limit < 1 || g.limit > GROUPS_MOST))\n throw new Error(`${at}, group has a \"limit\" that is not a whole number of groups from 1 to ${GROUPS_MOST}.`);\n // A msg() is said by the loader before a page is read; one that reaches here has no catalog to say it.\n if (isMessageRef(g.empty)) parseCaption(g.empty, `${at}.group.empty`);\n if (g.empty !== undefined && (typeof g.empty !== 'string' || !g.empty.trim() || g.empty.length > 60))\n throw new Error(`${at}, group has an \"empty\" that is not a short name for the rows with no value: 'Backlog'.`);\n let sum: string[] | undefined;\n if (g.sum !== undefined) {\n const named = typeof g.sum === 'string' ? [g.sum] : g.sum;\n if (\n !Array.isArray(named) ||\n named.length < 1 ||\n named.length > 2 ||\n named.some((c) => typeof c !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(c))\n )\n throw new Error(`${at}, group has a \"sum\" that is not one or two numeric columns of the rows: 'points' or ['points', 'hours'].`);\n if (new Set(named).size !== named.length) throw new Error(`${at}, group has a \"sum\" that names one column twice.`);\n sum = named as string[];\n }\n if (typeof s.limit === 'number' && s.limit > GROUP_ROWS_MOST)\n throw new Error(`${at} has a \"limit\" of ${s.limit} beside \"group\", where it is the rows to a group: at most ${GROUP_ROWS_MOST}.`);\n return {\n by: g.by.trim(),\n ...(g.show ? { show: g.show as string[] } : {}),\n ...(sort ? { sort } : {}),\n ...(g.limit === undefined ? {} : { limit: g.limit as number }),\n ...(g.empty === undefined ? {} : { empty: (g.empty as string).trim() }),\n ...(sum ? { sum } : {}),\n };\n}\n\n/* The record's header: the head the page already draws — its noun over the\n name, the verbs beside it — with the status chip and the facts line an\n author names. It is drawn in the head, not where it is written, so what\n would move it (a region, a tab, a width) is refused rather than ignored. */\nfunction parseHeader(s: Record<string, unknown>, at: string): PageSection {\n if (s.status !== undefined && (typeof s.status !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(s.status.trim()))) {\n throw new Error(`${at} has a \"status\" that is not one of the record's own columns, like \"stage\".`);\n }\n if (\n s.destructive !== undefined &&\n (!Array.isArray(s.destructive) || !s.destructive.length || s.destructive.some((id) => typeof id !== 'string' || !id.trim()))\n ) {\n throw new Error(`${at} has a \"destructive\" that is not a list of the record's action ids, like ['markLost'].`);\n }\n /* The one verb drawn filled: the one the record most needs next, which\n is never one that takes something away. Whether each id is one of the\n base resource's actions is the App's to judge (apps.ts), where both are known. */\n if (s.primary !== undefined && (typeof s.primary !== 'string' || !s.primary.trim())) {\n throw new Error(`${at} has a \"primary\" that is not the id of one of the record's actions, like 'renew'.`);\n }\n if (\n s.actions !== undefined &&\n (!Array.isArray(s.actions) ||\n !s.actions.length ||\n s.actions.some((id) => typeof id !== 'string' || !id.trim()) ||\n new Set(s.actions.map((id) => String(id).trim())).size !== s.actions.length)\n ) {\n throw new Error(`${at} has \"actions\" that are not a list of the record's action ids, each once, like ['resolve', 'reassign'].`);\n }\n /* The filled verb is one the head draws. */\n if (\n Array.isArray(s.actions) &&\n typeof s.primary === 'string' &&\n !s.actions.some((id) => String(id).trim() === (s.primary as string).trim())\n ) {\n throw new Error(`${at} has \"primary\" \"${s.primary}\", which is not one of its \"actions\": the head cannot fill a verb it does not draw.`);\n }\n if (\n typeof s.primary === 'string' &&\n Array.isArray(s.destructive) &&\n s.destructive.some((id) => typeof id === 'string' && id.trim() === (s.primary as string).trim())\n ) {\n throw new Error(\n `${at} names \"${s.primary}\" both \"primary\" and \"destructive\": the filled button is the forward verb, and a destructive one is drawn last and quiet.`,\n );\n }\n /* A header with only a `primary` or its `destructive` verbs is the head\n the page draws anyway, told which verb to fill: worth writing. One that\n says nothing is a slip. */\n if (['status', 'facts', 'primary', 'destructive', 'actions'].every((key) => s[key] === undefined)) {\n throw new Error(\n `${at}: a record header says something: a \"status\", \"facts\", its \"primary\" verb, its \"destructive\" ones or the \"actions\" it draws.`,\n );\n }\n const facts = s.facts === undefined ? undefined : parseFacts(s.facts, `${at}, header`);\n return {\n kind: 'fields',\n presentation: 'header',\n paths: facts?.facts ?? [],\n ...(facts?.factLabels ? { factLabels: facts.factLabels } : {}),\n ...(s.status === undefined ? {} : { status: (s.status as string).trim() }),\n ...(s.primary === undefined ? {} : { primary: (s.primary as string).trim() }),\n ...(s.destructive === undefined ? {} : { destructive: (s.destructive as string[]).map((id) => id.trim()) }),\n ...(s.actions === undefined ? {} : { headerActions: (s.actions as string[]).map((id) => id.trim()) }),\n };\n}\n\n/* A conversation's composer: the commands its two modes run, named as the App's actions name them. */\nfunction parseTimelineComposer(raw: unknown, at: string): TimelineComposer {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw))\n throw new Error(`${at} has a \"composer\" that is not { reply, note? }: the names of the commands a Reply and an Internal note run.`);\n const c = raw as Record<string, unknown>;\n allowKeys(c, ['reply', 'note', 'templates'], `${at}, composer`);\n for (const key of ['reply', 'note'] as const) {\n if (c[key] !== undefined && (typeof c[key] !== 'string' || !(c[key] as string).trim()))\n throw new Error(`${at}: composer.${key} is not a command's name.`);\n }\n if (typeof c.reply !== 'string') throw new Error(`${at}: a composer needs \"reply\", the command a Reply runs.`);\n const reply = c.reply.trim();\n const note = typeof c.note === 'string' ? c.note.trim() : undefined;\n if (note !== undefined && note === reply)\n throw new Error(\n `${at}: composer.reply and composer.note name the same command \"${reply}\"; a note is a different command from a reply.`,\n );\n const templates = c.templates === undefined ? undefined : parseComposerTemplates(c.templates, at);\n return { reply, ...(note === undefined ? {} : { note }), ...(templates ? { templates } : {}) };\n}\n\n/* Canned replies: the rows of a resource, each a label to pick and the text it puts in the box. */\nfunction parseComposerTemplates(raw: unknown, at: string): NonNullable<TimelineComposer['templates']> {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw))\n throw new Error(\n `${at}: composer.templates is not { from, labelColumn, text, query? }: the resource whose rows are canned replies, and their two columns.`,\n );\n const t = raw as Record<string, unknown>;\n allowKeys(t, ['from', 'labelColumn', 'text', 'query'], `${at}, composer.templates`, {\n label: 'is now \"labelColumn\": the column a reader picks a reply by',\n });\n for (const key of ['from', 'labelColumn', 'text'] as const) {\n if (typeof t[key] !== 'string' || !(t[key] as string).trim())\n throw new Error(\n `${at}: composer.templates needs \"${key}\", ${key === 'from' ? 'the resource whose rows are the canned replies' : key === 'labelColumn' ? 'the column a reader picks by' : 'the column whose words fill the box'}.`,\n );\n }\n /* Which rows are offered, beyond what the reader may read: a retired response is still readable. */\n if (t.query !== undefined && (typeof t.query !== 'string' || !t.query.trim()))\n throw new Error(`${at}: composer.templates has a \"query\" that is not a condition, like 'active = true'.`);\n return {\n from: (t.from as string).trim(),\n labelColumn: (t.labelColumn as string).trim(),\n text: (t.text as string).trim(),\n ...(typeof t.query === 'string' ? { query: t.query.trim() } : {}),\n };\n}\n\nconst TIMELINE_TONES = ['good', 'warn', 'bad', 'info', 'neutral'] as const;\n/* A tone rule says \"this column's value → a tone\": one or two columns at most, each a short list. */\nfunction parseTimelineTone(raw: unknown, at: string): Record<string, Record<string, TimelineTone>> {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw) || !Object.keys(raw).length) {\n throw new Error(`${at} has a \"tone\" that is not { column: { value: tone } }, like { kind: { internal: 'warn' } }.`);\n }\n const out: Record<string, Record<string, TimelineTone>> = {};\n for (const [column, values] of Object.entries(raw)) {\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(column))\n throw new Error(`${at}: tone names \"${column}\", which is not one of the stream's own columns.`);\n if (!values || typeof values !== 'object' || Array.isArray(values) || !Object.keys(values).length) {\n throw new Error(`${at}: tone.${column} is not { value: tone }, like { internal: 'warn' }.`);\n }\n out[column] = {};\n for (const [value, tone] of Object.entries(values)) {\n if (!(TIMELINE_TONES as readonly unknown[]).includes(tone))\n throw new Error(`${at}: tone.${column}.${value} is \"${String(tone)}\", not one of ${TIMELINE_TONES.join(', ')}.`);\n out[column][value] = tone as TimelineTone;\n }\n }\n return out;\n}\n\n/* Rows of these column values are one line: { kind: ['system'] }. */\nfunction parseTimelineCompact(raw: unknown, at: string): Record<string, string[]> {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw) || !Object.keys(raw).length) {\n throw new Error(`${at} has a \"compact\" that is not { column: [values] }, like { kind: ['system'] }.`);\n }\n const out: Record<string, string[]> = {};\n for (const [column, values] of Object.entries(raw)) {\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(column))\n throw new Error(`${at}: compact names \"${column}\", which is not one of the stream's own columns.`);\n if (!Array.isArray(values) || !values.length || values.some((value) => typeof value !== 'string' || !value)) {\n throw new Error(`${at}: compact.${column} is not a list of values, like ['system'].`);\n }\n out[column] = values.map(String);\n }\n return out;\n}\n\n/* Few enough columns that each still says why a row is on the list. */\nconst MATCH_MAX_SAME = 4;\nconst MATCH_MAX_NEAR = 2;\n\n/**\n * A list's match: the rows that share the record's own values, `same` equal\n * and `near` within a fraction. It is found by those values rather than by a\n * reference, so each word that only means something beside a reference is\n * refused here rather than ignored: there is no `whose` to walk, no parent\n * to `create` under (`empty` is kept: a match that finds nothing says so), and where the rows are the page's own table the record\n * is always left out.\n */\nfunction parseMatch(s: Record<string, unknown>, section: string): PageSimilar {\n /* Named by its title where it has one: a page may hold several related lists, and \"section 3\" alone is a count. */\n const at = typeof s.title === 'string' && s.title.trim() ? `${section} (\"${s.title.trim()}\")` : section;\n const beside: Record<string, string> = {\n whose: \"its rows share the record's values rather than point at it\",\n create:\n \"its rows are not the record's children: a row added here would need the record's values in every matched column, and a near column has no value to fill, so there is nothing to add them under (add through the list's own page)\",\n };\n for (const [key, why] of Object.entries(beside)) {\n if (s[key] !== undefined && s[key] !== false) throw new Error(`${at} has \"${key}\" beside \"same\" — ${why}.`);\n }\n const column = /^[A-Za-z_][A-Za-z0-9_]*$/;\n if (\n !Array.isArray(s.same) ||\n !s.same.length ||\n s.same.length > MATCH_MAX_SAME ||\n s.same.some((c) => typeof c !== 'string' || !column.test(c))\n ) {\n throw new Error(`${at}: \"same\" must be one to ${MATCH_MAX_SAME} columns a row shares with the record, e.g. ['body_type'].`);\n }\n const same = s.same as string[];\n if (new Set(same).size !== same.length) throw new Error(`${at}: \"same\" names a column twice.`);\n if (s.near === undefined) return { same };\n if (!s.near || typeof s.near !== 'object' || Array.isArray(s.near)) {\n throw new Error(`${at}: \"near\" must be an object of number columns and fractions, e.g. { price: 0.25 }.`);\n }\n const near = Object.entries(s.near as Record<string, unknown>);\n if (!near.length || near.length > MATCH_MAX_NEAR) {\n throw new Error(`${at}: \"near\" must name one or ${MATCH_MAX_NEAR === 2 ? 'two' : MATCH_MAX_NEAR} columns.`);\n }\n for (const [name, fraction] of near) {\n if (!column.test(name)) throw new Error(`${at}: \"near\" names \"${name}\", which is not a column name.`);\n if (same.includes(name)) throw new Error(`${at}: \"${name}\" is in both \"same\" and \"near\" — equal already is near.`);\n if (typeof fraction !== 'number' || !Number.isFinite(fraction) || fraction <= 0 || fraction > 1) {\n throw new Error(`${at}: \"near\" on \"${name}\" must be a fraction above 0 and at most 1 — 0.25 is within ±25% of the record's value.`);\n }\n }\n return { same, near: Object.fromEntries(near) as Record<string, number> };\n}\n\n/**\n * A list's `show`, in the nav's grammar: a column path, or `{ column,\n * label }` with the app's word over it. The paths stay the planner's\n * `columns`; the labels ride beside them for whoever draws the header.\n */\nfunction parseShowEntries(raw: unknown[], at: string): { columns: string[]; labels?: Record<string, string> } {\n const labels: Record<string, string> = {};\n const columns = raw.map((entry, i) => {\n if (typeof entry === 'string') return entry;\n if (entry && typeof entry === 'object' && !Array.isArray(entry)) {\n const c = entry as Record<string, unknown>;\n for (const key of Object.keys(c)) {\n if (key !== 'column' && key !== 'label') throw new Error(`${at}, \"show\" entry ${i + 1} has no \"${key}\". It takes: column, label.`);\n }\n if (typeof c.column !== 'string' || !c.column.trim()) throw new Error(`${at}, \"show\" entry ${i + 1} has no \"column\".`);\n if (c.label !== undefined && (typeof c.label !== 'string' || !c.label.trim())) {\n throw new Error(`${at}, \"show\" entry ${i + 1} has a \"label\" that is not a non-empty string.`);\n }\n if (c.label !== undefined) labels[c.column.trim()] = (c.label as string).trim();\n return c.column.trim();\n }\n throw new Error(`${at}, \"show\" entry ${i + 1} is not a column name or a { \"column\", \"label\" } object.`);\n });\n return { columns, ...(Object.keys(labels).length ? { labels } : {}) };\n}\n\n/**\n * A headline's calls to action: one or two, each opening one of the App's\n * views — a public form, a list, a dashboard — by its key: the landing's\n * call-to-action shape. Whether the key names such a view is judged where\n * the page meets the App.\n */\nfunction parseHeadlineActions(raw: unknown, at: string): PageHeadlineAction[] {\n if (!Array.isArray(raw) || !raw.length || raw.length > 2) throw new Error(`${at}: \"links\" must be one or two { label, to }.`);\n return raw.map((entry, i) => {\n const here = `${at}, link ${i + 1}`;\n if (!entry || typeof entry !== 'object' || Array.isArray(entry)) throw new Error(`${here} is not an object { label, to }.`);\n const a = entry as Record<string, unknown>;\n if (a.to !== undefined && typeof a.to !== 'string') {\n allowKeys(a, ['label', 'to', 'intent'], here, INTENT_RENAMED);\n if (typeof a.label !== 'string' || !a.label.trim() || a.label.trim().length > 60)\n throw new Error(`${here} needs a \"label\" of at most 60 characters.`);\n if (a.intent !== undefined && a.intent !== 'primary' && a.intent !== 'secondary')\n throw new Error(`${here} has an \"intent\" of \"${String(a.intent)}\". Use \"primary\" or \"secondary\".`);\n return {\n label: a.label.trim(),\n to: parseAppTarget(a.to, `${here}.to`, true),\n ...(a.intent === undefined ? {} : { intent: a.intent as 'primary' | 'secondary' }),\n };\n }\n allowKeys(a, ['label', 'to', 'intent', 'prefill', 'defaults', 'open'], here, INTENT_RENAMED);\n if (typeof a.label !== 'string' || !a.label.trim() || a.label.trim().length > 60)\n throw new Error(`${here} needs a \"label\" of at most 60 characters.`);\n if (typeof a.to !== 'string' || !/^[a-z0-9][a-z0-9-]{0,63}$/.test(a.to))\n throw new Error(`${here} needs a \"to\": the key of one of the App's views, or an appLink().`);\n if (a.intent !== undefined && a.intent !== 'primary' && a.intent !== 'secondary')\n throw new Error(`${here} has an \"intent\" of \"${String(a.intent)}\". Use \"primary\" or \"secondary\".`);\n /* A public form over the record, rather than a page of its own. Only\n 'modal': \"drawer\" is a list's word for a row, and a page is the default\n because it is what an address opens. */\n if (a.open !== undefined && a.open !== 'modal')\n throw new Error(`${here} has an \"open\" of \"${String(a.open)}\". Use \"modal\", or leave it out for a page.`);\n /* Where the form the action opens starts: an input filled from this\n record (input → field path), or with the author's words — `defaults`, the\n word a New form and a related list's create say a stated starting value with. */\n const inputs = (raw: unknown, key: 'prefill' | 'defaults'): Record<string, string> | undefined => {\n if (raw === undefined) return undefined;\n if (!raw || typeof raw !== 'object' || Array.isArray(raw) || !Object.keys(raw).length)\n throw new Error(`${here} has a \"${key}\" that is not an object of form input → ${key === 'prefill' ? 'field path' : 'text'}.`);\n return Object.fromEntries(\n Object.entries(raw as Record<string, unknown>).map(([input, value]) => {\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(input)) throw new Error(`${here}, ${key}: \"${input}\" is not a form input's name.`);\n if (key === 'prefill' && (typeof value !== 'string' || !FIELD_PATH.test(value.trim())))\n throw new Error(`${here}, prefill: \"${input}\" must name one of this record's fields.`);\n if (key === 'defaults' && (typeof value !== 'string' || !value.trim() || value.length > 500))\n throw new Error(`${here}, defaults: \"${input}\" must be text of at most 500 characters.`);\n return [input, (value as string).trim()];\n }),\n );\n };\n const prefill = inputs(a.prefill, 'prefill');\n const defaults = inputs(a.defaults, 'defaults');\n return {\n label: a.label.trim(),\n view: a.to,\n ...(a.intent === undefined ? {} : { intent: a.intent as 'primary' | 'secondary' }),\n ...(prefill ? { prefill } : {}),\n ...(defaults ? { defaults } : {}),\n ...(a.open === undefined ? {} : { open: 'modal' as const }),\n };\n });\n}\n\nfunction parseWidth(raw: unknown, at: string): SectionWidth | undefined {\n if (raw === undefined) return undefined;\n const width = String(raw);\n if (!WIDTHS.includes(width as SectionWidth)) {\n throw new Error(`${at} has a \"width\" of \"${width}\". Use ${WIDTHS.join(', ')}.`);\n }\n return width as SectionWidth;\n}\n\nfunction parseOffset(raw: unknown, at: string, side: string): SectionOffset | undefined {\n if (raw === undefined) return undefined;\n const offset = String(raw);\n if (!OFFSETS.includes(offset as SectionOffset)) {\n throw new Error(`${at} has an \"${side}\" of \"${offset}\". Use ${OFFSETS.join(', ')}.`);\n }\n return offset as SectionOffset;\n}\n\nfunction parseSort(raw: unknown, at: string): PageList['orderBy'] {\n return parseSortText(raw, at, 'due_on desc, name');\n}\n","import type { AppForm, FormLines } from './app.js';\nimport { columnKind, isFlagColumn, type ColumnMeta, type DatabaseSchema, type TableMeta } from '@mistriwork/engine/shared/schema.js';\nimport { autoKey, formShownFields, isRequired } from '@mistriwork/engine/shared/form.js';\nimport { unknownKey } from '@mistriwork/engine/shared/suggest.js';\nimport { computedColumnMeta } from '@mistriwork/engine/shared/computed.js';\nimport {\n LOCAL_FIELD,\n RESERVED,\n columnNamed,\n compileCondition,\n comparisonResult,\n conditionResult,\n evaluateFormCondition,\n family,\n formDefaultWord,\n normalize,\n rowValue,\n scalar,\n type CompiledCondition,\n type FormCondition,\n type FormOperator,\n type FormScalar,\n type FormStep,\n type FormStyle,\n} from '@mistriwork/engine/shared/formValues.js';\nexport {\n MAX_FORM_CONDITION,\n MAX_FORM_PREDICATES,\n evaluateFormCondition,\n formDefaultWord,\n inferredFormDefaults,\n type FormCondition,\n type FormScalar,\n type FormStep,\n type FormStyle,\n} from '@mistriwork/engine/shared/formValues.js';\nimport type { AppFormField, FormChildGroup } from '@mistriwork/engine/shared/servedApp.js';\nexport type { AppFormField, FormChildGroup } from '@mistriwork/engine/shared/servedApp.js';\n\n/** A validation rule: the record must meet `must`, a condition, wherever `when` (a condition too) holds. The\n * message is shown at the first column `must` names. */\nexport interface FormRule {\n must: FormCondition;\n when?: FormCondition;\n message: string;\n}\n\nexport const FORM_OPEN = ['drawer', 'page'] as const;\nexport const FORM_STYLES = ['stepper', 'intake'] as const;\n/** How a vocabulary or a flag may be drawn on a New form (`fields.<column>.as`). */\nconst FORM_FIELD_AS = ['cards', 'buttons', 'radios', 'select'] as const;\nexport const FORM_FIELD_KEYS = ['label', 'help', 'placeholder', 'multiline', 'as', 'where'] as const;\n/** A narrowed reference compares at most this many columns. */\nexport const MAX_FORM_WHERE = 4;\nconst MAX_FORM_DESCRIPTION = 500;\n\nexport const MAX_FORM_RULES = 128;\nexport const MAX_FORM_FIELDS = 256;\nexport const MAX_FORM_GROUPS = 32;\nexport const MAX_FORM_CHILDREN = 16;\nexport const MAX_FORM_CHILD_ROWS = 64;\nconst MAX_FORM_CHILD_ID = 64;\nexport const MAX_FORM_STEPS = 16;\nexport const MAX_FORM_STEP_FIELDS = 256;\nconst MAX_FORM_STEP_ID = 64;\nexport const FORM_UNRESOLVED_MESSAGE = 'This value could not be validated.';\n// eslint-disable-next-line no-control-regex -- Resource names must exclude ASCII control characters.\nconst RESOURCE = /^\\S(?:[^\\x00-\\x1f\\x7f]*\\S)?$/u;\n\nfunction fail(where: string): never {\n // Never stringify rejected data: it may contain row values or executable hooks.\n throw new Error(`${where}: invalid form declaration.`);\n}\n\nfunction frozen<T extends object>(value: T): T {\n return Object.freeze(value);\n}\nfunction object(raw: unknown, where: string, keys?: readonly string[]): Record<string, unknown> {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return fail(where);\n const proto = Object.getPrototypeOf(raw);\n if (proto !== Object.prototype && proto !== null) return fail(where);\n const descriptors = Object.getOwnPropertyDescriptors(raw);\n if (Reflect.ownKeys(raw).length !== Object.keys(descriptors).length) return fail(where);\n const result: Record<string, unknown> = {};\n for (const [key, descriptor] of Object.entries(descriptors)) {\n /* An unknown key is named with the keys the place takes, as allowKeys\n names one everywhere else. A key is authored; the values are what\n fail() must never echo. */\n if (keys && !keys.includes(key) && !RESERVED.has(key)) throw new Error(`${where} ${unknownKey(key, keys)}.`);\n if (RESERVED.has(key) || !descriptor.enumerable || !Object.hasOwn(descriptor, 'value') || (keys && !keys.includes(key)))\n return fail(where);\n Object.defineProperty(result, key, { value: descriptor.value, enumerable: true });\n }\n return result;\n}\n\nfunction list(raw: unknown, where: string, max: number, min = 0): unknown[] {\n if (!Array.isArray(raw) || raw.length < min || raw.length > max || Object.getPrototypeOf(raw) !== Array.prototype) return fail(where);\n const descriptors = Object.getOwnPropertyDescriptors(raw);\n if (Reflect.ownKeys(raw).length !== raw.length + 1) return fail(where);\n const result: unknown[] = [];\n for (let i = 0; i < raw.length; i++) {\n const descriptor = descriptors[String(i)];\n if (!descriptor || !descriptor.enumerable || !Object.hasOwn(descriptor, 'value')) return fail(where);\n result.push(descriptor.value);\n }\n return result;\n}\n\nfunction field(raw: unknown, where: string): string {\n if (typeof raw !== 'string' || !LOCAL_FIELD.test(raw) || RESERVED.has(raw)) return fail(where);\n return raw;\n}\n\nfunction text(raw: unknown, where: string): string {\n // eslint-disable-next-line no-control-regex -- Static labels/messages must not contain ASCII control characters.\n if (typeof raw !== 'string' || !raw.trim() || raw.length > 256 || /[\\x00-\\x1f\\x7f]/u.test(raw)) return fail(where);\n return raw;\n}\n\nfunction childId(raw: unknown, where: string): string {\n if (typeof raw !== 'string' || raw.length > MAX_FORM_CHILD_ID || !/^[a-z][a-z0-9_-]*$/u.test(raw)) return fail(where);\n return raw;\n}\n\nfunction stepId(raw: unknown, where: string): string {\n if (typeof raw !== 'string' || raw.length > MAX_FORM_STEP_ID || !/^[a-z][a-z0-9_-]*$/u.test(raw)) return fail(where);\n return raw;\n}\n\n/** The id `step()` gives a step: its label's slug. */\nfunction stepSlug(label: string): string {\n return label\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-|-$/g, '');\n}\n\n/* A step written as an object may leave its id out, as `step()` does; the\n label's slug is its id then — a step that lists fields and one that saves\n through its own command alike (appSavedForms.ts), and a label with no slug that can be one\n (`'1. Basics'`) says what to write rather than failing opaquely. */\nexport function stepIdOf(s: Record<string, unknown>, label: string, where: string): string {\n if (Object.hasOwn(s, 'id')) return stepId(s.id, where);\n const slug = stepSlug(label);\n if (!/^[a-z][a-z0-9_-]*$/u.test(slug) || slug.length > MAX_FORM_STEP_ID) {\n throw new Error(`${where}: step \"${label}\" needs an \"id\" — lowercase letters, digits, \"_\" and \"-\", starting with a letter.`);\n }\n return slug;\n}\n\n/** Author prose that is not a row value: one sentence or a few, plain text. */\nfunction sentence(raw: unknown, where: string, max: number): string {\n // eslint-disable-next-line no-control-regex -- Static copy must not contain ASCII control characters.\n if (typeof raw !== 'string' || !raw.trim() || raw.length > max || /[\\x00-\\x1f\\x7f]/u.test(raw)) {\n throw new Error(`${where} is not plain text of at most ${max} characters.`);\n }\n return raw.trim();\n}\n\nfunction parseFormField(raw: unknown, name: string, where: string): AppFormField {\n const here = `${where}, fields \"${name}\"`;\n const f = object(raw, here, FORM_FIELD_KEYS);\n const result: AppFormField = {};\n for (const key of ['label', 'help', 'placeholder'] as const) {\n if (Object.hasOwn(f, key)) result[key] = sentence(f[key], `${here}, \"${key}\"`, 256);\n }\n if (Object.hasOwn(f, 'multiline')) {\n if (typeof f.multiline !== 'boolean') throw new Error(`${here}, \"multiline\" is true or false.`);\n result.multiline = f.multiline;\n }\n if (Object.hasOwn(f, 'as')) {\n if (typeof f.as !== 'string' || !FORM_FIELD_AS.includes(f.as as (typeof FORM_FIELD_AS)[number])) {\n throw new Error(`${here}, \"as\" is one of ${FORM_FIELD_AS.map((w) => `\"${w}\"`).join(', ')}.`);\n }\n result.as = f.as as (typeof FORM_FIELD_AS)[number];\n }\n if (Object.hasOwn(f, 'where')) {\n const pairs = Object.entries(object(f.where, `${here}, \"where\"`));\n if (!pairs.length || pairs.length > MAX_FORM_WHERE) {\n throw new Error(`${here}, \"where\" names one to ${MAX_FORM_WHERE} columns: { project_id: 'project_id' }.`);\n }\n result.where = frozen(Object.fromEntries(pairs.map(([target, local]) => [field(target, here), field(local, here)])));\n }\n return frozen(result);\n}\n\n/** A condition, checked as the query language and kept as its text. */\nexport function parseFormCondition(raw: unknown, where = 'Condition', paths = false): FormCondition {\n compileCondition(raw, where, undefined, 'record', paths);\n return (raw as string).trim();\n}\n\nexport function parseFormRules(raw: unknown, where = 'Validation'): FormRule[] {\n return frozen(\n list(raw, where, MAX_FORM_RULES).map((rawRule) => {\n const r = object(rawRule, where, ['must', 'when', 'message']);\n if (!Object.hasOwn(r, 'must')) return fail(where);\n return frozen({\n must: parseFormCondition(r.must, `${where}, \"must\"`, true),\n message: text(r.message, where),\n ...(Object.hasOwn(r, 'when') ? { when: parseFormCondition(r.when, `${where}, \"when\"`, true) } : {}),\n });\n }),\n );\n}\n\n/** The column a rule's message is shown at: the first one its `must` compares, or the reference it reads through. */\nfunction ruleField(rule: FormRule): string {\n return compileCondition(rule.must, 'Validation', undefined, 'record', true)[0][0].field.split('.')[0];\n}\n\nexport function parseAppValidation(raw: unknown, where = 'Validation'): Record<string, FormRule[]> {\n const declarations = object(raw, where);\n if (Object.keys(declarations).length > MAX_FORM_FIELDS) return fail(where);\n return frozen(\n Object.fromEntries(\n Object.entries(declarations).map(([resource, rules]) => {\n if (!RESOURCE.test(resource) || resource.length > 256) return fail(where);\n return [resource, parseFormRules(rules, where)];\n }),\n ),\n );\n}\n\n/** The most fields side by side in one row. */\nconst MAX_FORM_ROW = 3;\nconst STEP_WORDS = { title: 160, lead: 500, next: 40, skip: 40 } as const;\n/** The keys a step takes beside its id, label and fields. */\nconst STEP_WORD_KEYS = Object.keys(STEP_WORDS) as readonly (keyof typeof STEP_WORDS)[];\n\n/** A step's own words for the intake style: its heading, the sentence under it, and its button's. */\nexport function parseStepWords(\n s: Record<string, unknown>,\n where: string,\n keys: readonly (keyof typeof STEP_WORDS)[] = STEP_WORD_KEYS,\n): Pick<FormStep, 'title' | 'lead' | 'next' | 'skip'> {\n const result: Pick<FormStep, 'title' | 'lead' | 'next' | 'skip'> = {};\n for (const key of keys) if (Object.hasOwn(s, key)) result[key] = sentence(s[key], `${where}, \"${key}\"`, STEP_WORDS[key]);\n return result;\n}\n\n/**\n * Fields as authored, for a step, a group or the form's `show`: names, and inner lists that are rows\n * (`[['first_name', 'last_name'], 'dob']`). The fields are the names read through, in order, the list everything\n * else reads; `rows` is kept beside them only where some row holds more than one.\n */\nfunction parseRowFields(raw: unknown, where: string, max: number, min: number): { fields: string[]; rows?: string[][] } {\n const rows = list(raw, where, max, min).map((entry, i): string[] => {\n if (!Array.isArray(entry)) return [field(entry, where)];\n const at = `${where}, field ${i + 1}`;\n if (!entry.length) throw new Error(`${at} is an empty row. A row holds one to ${MAX_FORM_ROW} fields.`);\n if (entry.length > MAX_FORM_ROW)\n throw new Error(\n `${at} is a row of ${entry.length} fields; a row holds at most ${MAX_FORM_ROW}, side by side. Split it into two rows.`,\n );\n if (entry.some((member) => Array.isArray(member)))\n throw new Error(`${at} nests a row inside a row. A row is one list of field names: [['first_name', 'last_name'], 'dob'].`);\n return list(entry, at, MAX_FORM_ROW, 1).map((member) => field(member, where));\n });\n const fields = rows.flat();\n if (fields.length > max) return fail(where);\n const twice = fields.find((name, i) => fields.indexOf(name) !== i);\n if (twice !== undefined) throw new Error(`${where} names \"${twice}\" twice; a field stands in one place on the form.`);\n return {\n fields: frozen(fields),\n ...(rows.some((row) => row.length > 1) ? { rows: frozen(rows.map((row) => frozen(row))) } : {}),\n };\n}\n\n/**\n * A list read again from a parsed model: its flat `fields` and the `rows` kept beside them, which must say the same\n * names in the same order. Authored, the rows are the list itself.\n */\nfunction rowFields(\n raw: Record<string, unknown>,\n key: string,\n rowsKey: string,\n where: string,\n parsed: boolean,\n max: number,\n min: number,\n): { fields: string[]; rows?: string[][] } {\n if (!parsed || !Object.hasOwn(raw, rowsKey)) return parseRowFields(raw[key], where, max, min);\n const read = parseRowFields(raw[rowsKey], where, max, min);\n const flat = list(raw[key], where, max, min);\n if (flat.length !== read.fields.length || flat.some((name, i) => name !== read.fields[i])) return fail(where);\n return read;\n}\n\n/** The keys an authored resource form takes (children stay behind the shared parser). */\nconst FORM_KEYS = [\n 'show',\n 'defaults',\n 'derived',\n 'groups',\n 'when',\n 'steps',\n 'style',\n 'description',\n 'open',\n 'fields',\n 'lines',\n 'live',\n] as const;\n/** What an author writes: `derived` is read out of `defaults` (`{ from: 'service_id.price' }`), never said itself. */\nexport const AUTHORED_FORM_KEYS = FORM_KEYS.filter((key) => key !== 'derived');\n/** A value taken from a referenced record: one reference of this table, then a column of the table it names. */\nconst DERIVED_PATH = /^[A-Za-z_][A-Za-z0-9_]{0,127}\\.[A-Za-z_][A-Za-z0-9_]{0,127}$/;\n\n/** The most collections of child rows one New form asks, and the most names it says live. */\nconst MAX_FORM_LINES = 4;\nconst MAX_FORM_LIVE = 16;\nconst LINE_KEYS = ['label', 'from', 'whose', 'show', 'order', 'min', 'max'] as const;\nconst IDENT = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;\n\n/**\n * A New form's child rows — an order's lines, a bundle's items — asked as a\n * grid under the record's fields and inserted with it, in its transaction:\n * `lines: [{ label: 'Items', from: 'order_line', whose: 'order_id', show: ['product_id', 'qty', 'unit_price'] }]`.\n * The words are a related list's (`related(title, from, whose, { show })`), and its caption is a `label` as a group's is;\n * the catalog checks them where the form meets it (formLines.ts).\n */\nfunction parseFormLines(raw: unknown, where: string): FormLines[] {\n if (!Array.isArray(raw) || !raw.length || raw.length > MAX_FORM_LINES)\n throw new Error(\n `${where}: form.lines is a list of one to ${MAX_FORM_LINES} collections of child rows, each { label, from, whose, show }.`,\n );\n const seen = new Set<string>();\n return frozen(\n raw.map((entry, i) => {\n const at = `${where}: form.lines ${i + 1}`;\n if (!entry || typeof entry !== 'object' || Array.isArray(entry)) throw new Error(`${at} is not { label, from, whose, show }.`);\n const l = entry as Record<string, unknown>;\n if (l.title !== undefined) throw new Error(`${at} has \"title\", which is now \"label\", as a group's caption is.`);\n const extra = Object.keys(l).find((key) => !(LINE_KEYS as readonly string[]).includes(key));\n if (extra !== undefined) throw new Error(`${at} ${unknownKey(extra, LINE_KEYS)}.`);\n if (typeof l.label !== 'string' || !l.label.trim() || l.label.length > 80)\n throw new Error(`${at} needs a \"label\" of at most 80 characters.`);\n for (const key of ['from', 'whose'] as const)\n if (typeof l[key] !== 'string' || !IDENT.test(l[key] as string))\n throw new Error(\n `${at} needs \"${key}\": ${key === 'from' ? 'the resource of the rows' : 'the column of those rows that points at this record'}.`,\n );\n if (\n !Array.isArray(l.show) ||\n !l.show.length ||\n l.show.length > 12 ||\n l.show.some((column) => typeof column !== 'string' || !IDENT.test(column)) ||\n new Set(l.show).size !== l.show.length\n )\n throw new Error(`${at} needs \"show\": one to 12 of the row's own columns, each once.`);\n if (l.order !== undefined && (typeof l.order !== 'string' || !IDENT.test(l.order)))\n throw new Error(`${at} has an \"order\" that is not a column of the rows: the number each row's place is kept in.`);\n const bound = (key: 'min' | 'max') => {\n const v = l[key];\n if (v === undefined) return undefined;\n if (typeof v !== 'number' || !Number.isInteger(v) || v < (key === 'max' ? 1 : 0) || v > MAX_FORM_CHILD_ROWS)\n throw new Error(`${at} has a \"${key}\" that is not a whole number of rows up to ${MAX_FORM_CHILD_ROWS}.`);\n return v;\n };\n const min = bound('min'),\n max = bound('max');\n if (min !== undefined && max !== undefined && min > max) throw new Error(`${at} has a \"min\" above its \"max\".`);\n if (seen.has(l.from as string))\n throw new Error(`${at} asks the rows of \"${l.from as string}\" a second time; one collection per resource.`);\n seen.add(l.from as string);\n return frozen({\n label: l.label.trim(),\n from: l.from as string,\n whose: l.whose as string,\n show: frozen([...(l.show as string[])]),\n ...(l.order === undefined ? {} : { order: l.order as string }),\n ...(min === undefined ? {} : { min }),\n ...(max === undefined ? {} : { max }),\n });\n }),\n );\n}\n\n/** The resource's computed columns and rollups a New form says as its fields and lines are typed: `live: ['subtotal', 'total']`. */\nfunction parseFormLive(raw: unknown, where: string): string[] {\n if (\n !Array.isArray(raw) ||\n !raw.length ||\n raw.length > MAX_FORM_LIVE ||\n raw.some((name) => typeof name !== 'string' || !IDENT.test(name)) ||\n new Set(raw).size !== raw.length\n )\n throw new Error(`${where}: form.live is a list of one to ${MAX_FORM_LIVE} of the resource's computed columns and rollups, each once.`);\n return frozen([...(raw as string[])]);\n}\n\n/** `parsed`: the form is a model already read (its `show` derived from its groups), not an authored one. */\nfunction parseAppFormShape(raw: unknown, where: string, parsed = false): Omit<AppForm, 'children'> {\n /* A parsed model carries its rows beside its flat lists (`showRows`, a group's and a step's `rows`); an author writes\n them inside the lists. */\n const f = object(raw, where, parsed ? [...FORM_KEYS, 'showRows'] : FORM_KEYS);\n const result: AppForm = {};\n /* `show` is what the form asks. Its steps, or else its groups, are that list already, so saying both is two lists to keep\n alike: it is the one or the other. */\n if (Object.hasOwn(f, 'show')) {\n if (parsed && Object.hasOwn(f, 'groups') && !Object.hasOwn(f, 'steps')) {\n /* derived again below */\n } else if (Object.hasOwn(f, 'steps') || Object.hasOwn(f, 'groups')) {\n throw new Error(\n `${where} has \"show\" and ${Object.hasOwn(f, 'steps') ? 'steps' : 'groups'}: the form asks the fields of its ${Object.hasOwn(f, 'steps') ? 'steps' : 'groups'}. Keep one.`,\n );\n }\n const { fields, rows } = rowFields(f, 'show', 'showRows', where, parsed, MAX_FORM_FIELDS, 0);\n result.show = fields;\n if (rows) result.showRows = rows;\n }\n if (Object.hasOwn(f, 'defaults')) {\n const defaults = object(f.defaults, where);\n if (Object.keys(defaults).length > MAX_FORM_FIELDS) return fail(where);\n result.defaults = frozen(Object.fromEntries(Object.entries(defaults).map(([key, value]) => [field(key, where), scalar(value, where)])));\n }\n if (Object.hasOwn(f, 'derived')) {\n const derived = object(f.derived, where);\n if (Object.keys(derived).length > MAX_FORM_FIELDS) return fail(where);\n result.derived = frozen(\n Object.fromEntries(\n Object.entries(derived).map(([key, path]) => {\n if (typeof path !== 'string' || !DERIVED_PATH.test(path))\n throw new Error(\n `${where}: defaults \"${key}\" is from ${JSON.stringify(path)}; a value from a record is { from: 'reference.column' }.`,\n );\n if (Object.hasOwn(result.defaults ?? {}, key)) throw new Error(`${where}: defaults \"${key}\" is said twice.`);\n return [field(key, where), path];\n }),\n ),\n );\n }\n if (Object.hasOwn(f, 'groups')) {\n const seen = new Set<string>();\n result.groups = frozen(\n list(f.groups, where, MAX_FORM_GROUPS).map((rawGroup) => {\n const g = object(rawGroup, where, parsed ? ['label', 'description', 'fields', 'rows'] : ['label', 'description', 'fields']);\n const { fields, rows } = rowFields(g, 'fields', 'rows', where, parsed, MAX_FORM_FIELDS, 1);\n for (const name of fields) {\n if (seen.has(name)) return fail(where);\n seen.add(name);\n }\n const label = text(g.label, where);\n return frozen({\n label,\n ...(Object.hasOwn(g, 'description')\n ? { description: sentence(g.description, `${where}, group \"${label}\", \"description\"`, 256) }\n : {}),\n fields,\n ...(rows ? { rows } : {}),\n });\n }),\n );\n }\n /* The groups carve up what the form asks, in their order; with steps, the steps' fields are the list (formShownFields). */\n if (result.groups && !Object.hasOwn(f, 'steps')) {\n result.show = frozen(result.groups.flatMap((group) => [...group.fields]));\n /* Their rows are the form's, read in the same order; a group without rows is one field to a row. */\n if (result.groups.some((group) => group.rows))\n result.showRows = frozen(result.groups.flatMap((group) => group.rows ?? group.fields.map((name) => frozen([name]))));\n }\n if (Object.hasOwn(f, 'when')) {\n const conditions = object(f.when, where);\n if (Object.keys(conditions).length > MAX_FORM_FIELDS) return fail(where);\n result.when = frozen(\n Object.fromEntries(\n Object.entries(conditions).map(([key, value]) => {\n const name = field(key, where);\n if (result.show && !result.show.includes(name)) return fail(where);\n return [name, parseFormCondition(value, `${where}, when \"${name}\"`)];\n }),\n ),\n );\n }\n if (Object.hasOwn(f, 'steps')) {\n const seenIds = new Set<string>();\n const owned = new Set<string>();\n result.steps = frozen(\n list(f.steps, where, MAX_FORM_STEPS, 1).map((rawStep) => {\n const s = object(rawStep, where, ['id', 'label', 'fields', ...(parsed ? ['rows'] : []), ...STEP_WORD_KEYS]);\n const label = text(s.label, where);\n const id = stepIdOf(s, label, where);\n if (seenIds.has(id)) throw new Error(`${where}: two steps are \"${id}\"; give one an \"id\" of its own.`);\n seenIds.add(id);\n const { fields, rows } = rowFields(s, 'fields', 'rows', where, parsed, MAX_FORM_STEP_FIELDS, 1);\n if (fields.some((name) => owned.has(name))) return fail(where);\n fields.forEach((name) => owned.add(name));\n return frozen({ id, label, fields, ...(rows ? { rows } : {}), ...parseStepWords(s, `${where}, step \"${id}\"`) });\n }),\n );\n /* A row stands inside one group, as a group stands inside one step: the group's heading sits over the row, and\n the fields a group leaves out are drawn after every group. */\n if (result.groups) {\n const groupOf = new Map(result.groups.flatMap((group, at) => group.fields.map((name) => [name, at] as const)));\n for (const step of result.steps)\n for (const row of step.rows ?? []) {\n if (new Set(row.map((name) => groupOf.get(name) ?? -1)).size > 1)\n throw new Error(\n `${where}: step \"${step.id}\" puts ${row.map((name) => `\"${name}\"`).join(', ')} in one row across groups. A row stands inside one group.`,\n );\n }\n }\n }\n if (Object.hasOwn(f, 'style')) {\n if (!(FORM_STYLES as readonly unknown[]).includes(f.style)) throw new Error(`${where}, \"style\" is 'stepper' or 'intake'.`);\n if (!Object.hasOwn(f, 'steps')) throw new Error(`${where}, \"style\" draws steps, and this form has none.`);\n result.style = f.style as FormStyle;\n }\n if (Object.hasOwn(f, 'description')) result.description = sentence(f.description, `${where}, \"description\"`, MAX_FORM_DESCRIPTION);\n if (Object.hasOwn(f, 'open')) {\n if (!(FORM_OPEN as readonly unknown[]).includes(f.open)) throw new Error(`${where}, \"open\" is 'drawer' or 'page'.`);\n result.open = f.open as AppForm['open'];\n }\n if (Object.hasOwn(f, 'lines')) result.lines = parseFormLines(f.lines, where);\n if (Object.hasOwn(f, 'live')) result.live = parseFormLive(f.live, where);\n if (Object.hasOwn(f, 'fields')) {\n const declared = Object.entries(object(f.fields, `${where}, \"fields\"`));\n if (declared.length > MAX_FORM_FIELDS) return fail(where);\n result.fields = frozen(Object.fromEntries(declared.map(([name, value]) => [field(name, where), parseFormField(value, name, where)])));\n }\n return frozen(result);\n}\n\nfunction parseFormChildren(raw: unknown, where = 'Form, children'): FormChildGroup[] {\n const seen = new Set<string>();\n return frozen(\n list(raw, where, MAX_FORM_CHILDREN).map((rawChild) => {\n const c = object(rawChild, where, ['id', 'label', 'table', 'relation', 'min', 'max', 'form']);\n const id = childId(c.id, where);\n if (seen.has(id)) return fail(where);\n seen.add(id);\n const relation = object(c.relation, where, ['from', 'to']);\n const min = c.min === undefined ? 0 : c.min;\n const max = c.max;\n if (\n typeof min !== 'number' ||\n typeof max !== 'number' ||\n !Number.isInteger(min) ||\n min < 0 ||\n min > MAX_FORM_CHILD_ROWS ||\n !Number.isInteger(max) ||\n max < 1 ||\n max > MAX_FORM_CHILD_ROWS ||\n min > max\n )\n return fail(where);\n if (typeof c.table !== 'string' || !RESOURCE.test(c.table) || c.table.length > 256) return fail(where);\n const captured: FormChildGroup = {\n id,\n label: text(c.label, where),\n table: c.table,\n relation: { from: field(relation.from, where), to: field(relation.to, where) },\n ...(min ? { min } : {}),\n max,\n ...(c.form === undefined ? {} : { form: parseAppFormShape(c.form, `${where} \"${id}\", form`) }),\n };\n return frozen(captured);\n }),\n );\n}\n\nexport function parseAppForm(raw: unknown, where = 'Form', parsed = false): AppForm {\n const input = object(raw, where, [...FORM_KEYS, ...(parsed ? ['showRows'] : []), 'children']);\n const shape = parseAppFormShape(Object.fromEntries(Object.entries(input).filter(([key]) => key !== 'children')), where, parsed);\n if (!Object.hasOwn(input, 'children')) return shape as AppForm;\n return frozen({ ...shape, children: parseFormChildren(input.children, `${where}, children`) });\n}\n\nexport function referencedConditionFields(condition: FormCondition, paths = false): string[] {\n return [\n ...new Set(\n compileCondition(condition, 'Condition', undefined, 'record', paths)\n .flat()\n .map((p) => p.field),\n ),\n ];\n}\n\nexport function referencedRuleFields(rules: readonly FormRule[]): string[] {\n return [\n ...new Set(\n parseFormRules(rules).flatMap((rule) => [\n ...compileCondition(rule.must, 'Validation', undefined, 'record', true)\n .flat()\n .flatMap((p) => [p.field, ...(p.other === undefined ? [] : [p.other])]),\n ...(rule.when ? referencedConditionFields(rule.when, true) : []),\n ]),\n ),\n ];\n}\n\n/** A record as a rule reads it: a text column holding only blanks is as empty as a missing one. */\nfunction blanksAsEmpty(row: Readonly<Record<string, unknown>>, columns: readonly ColumnMeta[]): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n for (const column of columns) {\n const value = rowValue(row, column.name);\n result[column.name] = typeof value === 'string' && !value.trim() && family(column) === 'text' ? null : value;\n }\n return result;\n}\n\nfunction comparisonProblem(column: ColumnMeta, op: FormOperator): boolean {\n const kind = family(column);\n return !kind || ((kind === 'text' || kind === 'boolean') && op !== 'eq' && op !== 'ne');\n}\n\nfunction conditionProblems(condition: FormCondition, table: TableMeta, paths = false): string[] {\n let compiled: CompiledCondition;\n try {\n compiled = compileCondition(\n condition,\n `Condition \"${condition}\"`,\n table.columns,\n /^\\w+$/.test(table.name) ? table.name : undefined,\n paths,\n );\n } catch (error) {\n return [(error as Error).message];\n }\n return compiled.flat().flatMap((p) => {\n const column = columnNamed(table.columns, p.field);\n if (p.other !== undefined) {\n const other = columnNamed(table.columns, p.other);\n return !column || !other || family(column) !== family(other) || comparisonProblem(column, p.operator)\n ? [`Condition on \"${p.field}\" is not comparable with \"${p.other}\" in this schema.`]\n : [];\n }\n return !column ||\n comparisonProblem(column, p.operator) ||\n p.values.some((value) => normalize(column, value) === undefined || (value === null && p.operator !== 'eq' && p.operator !== 'ne'))\n ? [`Condition on \"${p.field}\" is not comparable with this schema.`]\n : [];\n });\n}\n\n/** The `reference.column` paths a validation reads through its references, each once. */\nexport function ruleReferencePaths(rules: readonly FormRule[]): string[] {\n return referencedRuleFields(rules).filter((field) => field.includes('.'));\n}\n\n/**\n * The columns a validation is judged against: the table's own, and each column it reads through a reference,\n * named by its path and typed as the referenced column is. `problems` says each path the catalog cannot follow.\n */\nexport function ruleColumns(\n rules: readonly FormRule[],\n table: TableMeta,\n schema: DatabaseSchema | undefined,\n): { columns: ColumnMeta[]; problems: string[] } {\n const columns = [...table.columns];\n const problems: string[] = [];\n for (const path of ruleReferencePaths(rules)) {\n const source = derivedSource(path, table, schema);\n if ('problem' in source) problems.push(`Validation reads \"${path}\": ${source.problem}`);\n else columns.push({ ...source.column, name: path, primaryKey: false });\n }\n return { columns, problems };\n}\n\nexport function validateFormRules(rules: readonly FormRule[], table: TableMeta, schema?: DatabaseSchema): string[] {\n const captured = parseFormRules(rules);\n // Without a catalog a path cannot be followed, and is left to the check that has one.\n if (!schema && ruleReferencePaths(captured).length) return [];\n const { columns, problems } = ruleColumns(captured, table, schema);\n if (problems.length) return problems;\n const judged = { ...table, columns };\n for (const rule of captured) {\n problems.push(...conditionProblems(rule.must, judged, true));\n if (rule.when) problems.push(...conditionProblems(rule.when, judged, true));\n }\n return [...new Set(problems)];\n}\n\n/** What a form is judged against beyond its own table: the catalog its references\n * point into, and who `me` can be — `null` for an App without sign-in, the\n * `auth.me` table's id (or undefined, email only) where people sign in. */\ninterface AppFormContext {\n schema?: DatabaseSchema;\n /** The catalog a value from a referenced record is read from: `schema` with its computed columns (app.ts storedComputed). */\n derivedFrom?: DatabaseSchema;\n person?: { table?: string } | null;\n}\n\n/** Use the current readable projection; this helper never grants field access. */\nexport function validateAppForm(\n form: AppForm,\n table: TableMeta,\n readableFields: readonly string[] = table.columns.map((c) => c.name),\n context: AppFormContext = {},\n): string[] {\n const captured = parseAppForm(form, 'Form', true);\n const readable = new Set(readableFields);\n const stepFields = (captured.steps ?? []).flatMap((step) => step.fields);\n const shownFields = formShownFields(captured);\n const presentationFields = [\n ...(captured.show ?? []),\n ...Object.keys(captured.defaults ?? {}),\n ...(captured.groups ?? []).flatMap((group) => group.fields),\n ...Object.keys(captured.when ?? {}),\n ...stepFields,\n ...Object.keys(captured.fields ?? {}),\n ...Object.values(captured.fields ?? {}).flatMap((f) => Object.values(f?.where ?? {})),\n ];\n const problems: string[] = [];\n const defaults = captured.defaults ?? {};\n const named = [...(captured.show ?? []), ...Object.keys(defaults)].filter((name) => !columnNamed(table.columns, name));\n const assignedDefaults = table.columns.filter((c) => c.identity === 'always' && Object.hasOwn(defaults, c.name));\n const nullDefaults = table.columns.filter((c) => !c.nullable && Object.hasOwn(defaults, c.name) && defaults[c.name] === null);\n if (named.length) problems.push(`Form: \"${table.name}\" has no column ${named.map((name) => `\"${name}\"`).join(', ')}.`);\n if (assignedDefaults.length)\n problems.push(\n `Form: ${assignedDefaults.map((c) => `\"${c.name}\"`).join(', ')} is always assigned by the database and cannot have a form default.`,\n );\n if (nullDefaults.length) problems.push(`Form: ${nullDefaults.map((c) => `\"${c.name}\"`).join(', ')} cannot default to null (NOT NULL).`);\n for (const [name, path] of Object.entries(captured.derived ?? {}))\n problems.push(...derivedProblems(name, path, table, context.derivedFrom ?? context.schema));\n if (shownFields) {\n const shown = new Set([...shownFields, ...Object.keys(defaults), ...Object.keys(captured.derived ?? {})]);\n const assigned = autoKey(table);\n const starved = table.columns.filter((c) => isRequired(c, assigned) && !shown.has(c.name));\n if (starved.length)\n problems.push(\n `Form: \"${table.name}\" requires ${starved.map((c) => `\"${c.name}\"`).join(', ')} (NOT NULL, no default) — show it, or give it a default.`,\n );\n }\n if (captured.steps) {\n const shown = new Set(captured.show ?? stepFields);\n const owned = new Set(stepFields);\n const missingOwnership = [...shown].filter((name) => !owned.has(name));\n const unshown = [...owned].filter((name) => !shown.has(name));\n if (missingOwnership.length || unshown.length) {\n problems.push('Form steps must own exactly the fields in show.');\n }\n const owner = new Map<string, string>();\n for (const step of captured.steps) for (const name of step.fields) owner.set(name, step.id);\n for (const group of captured.groups ?? []) {\n const stepIds = new Set(group.fields.map((name) => owner.get(name)).filter((id): id is string => id !== undefined));\n if (stepIds.size > 1) problems.push(`Form group \"${group.label}\" cannot span multiple steps.`);\n if (group.fields.some((name) => !owner.has(name)))\n problems.push(`Form group \"${group.label}\" contains a field outside the form steps.`);\n }\n }\n for (const name of new Set(presentationFields)) {\n if (!columnNamed(table.columns, name) || !readable.has(name)) problems.push(`Form field \"${name}\" is unknown or hidden.`);\n }\n /* A field may be shown while a computed column holds: the form works it out from the draft as it is filled in\n (newRecordModel.ts), so the condition reads it as a column, as the browser does. */\n const computed = table.computed ?? {};\n const judged = Object.keys(computed).length\n ? { ...table, columns: [...table.columns, ...Object.entries(computed).map(([name, value]) => computedColumnMeta(name, value))] }\n : table;\n for (const condition of Object.values(captured.when ?? {})) {\n if (!condition) continue;\n problems.push(...conditionProblems(condition, judged));\n if (referencedConditionFields(condition).some((name) => !readable.has(name) && !Object.hasOwn(computed, name))) {\n problems.push('Form visibility depends on an unreadable field.');\n }\n }\n const shownNow = new Set(shownFields ?? table.columns.map((c) => c.name));\n for (const [name, dress] of Object.entries(captured.fields ?? {})) {\n const column = columnNamed(table.columns, name);\n if (!column || !dress) continue;\n if (!shownNow.has(name)) problems.push(`Form: fields \"${name}\" is not a field this form asks for; add it to show, or drop its dress.`);\n if (dress.multiline !== undefined && family(column) !== 'text')\n problems.push(`Form: fields \"${name}\" is ${column.type || 'untyped'}; only a text field is drawn on one line or several.`);\n if (dress.as !== undefined) {\n const vocabulary = Boolean(column.allowed?.length);\n if (!vocabulary && !isFlagColumn(column))\n problems.push(\n `Form: fields \"${name}\" is ${column.type || 'untyped'}; \"as\" draws a column with a vocabulary (an enum or a check-constrained text) or a flag.`,\n );\n else if (!vocabulary && (dress.as === 'cards' || dress.as === 'radios'))\n problems.push(\n `Form: fields \"${name}\" is a flag; \"as: '${dress.as}'\" needs a vocabulary, a flag is drawn as \"buttons\" or \"select\".`,\n );\n }\n if (dress.where) problems.push(...narrowingProblems(name, column, dress.where, table, context.schema));\n }\n for (const [name, value] of Object.entries(defaults)) {\n const column = columnNamed(table.columns, name);\n if (column) problems.push(...defaultWordProblems(name, column, value, context));\n }\n return [...new Set(problems)];\n}\n\n/* A narrowed picker is the reference's choices, where the referenced row\n agrees with this record on one to four columns. The server holds every\n write to it (appValidation.ts), so what it names must be real columns. */\nfunction narrowingProblems(\n name: string,\n column: ColumnMeta,\n where: Record<string, string>,\n table: TableMeta,\n schema?: DatabaseSchema,\n): string[] {\n if (!column.references)\n return [`Form: fields \"${name}\" narrows with \"where\", which only a reference takes; \"${name}\" refers to nothing.`];\n const problems: string[] = [];\n for (const local of Object.values(where)) {\n if (local === name) problems.push(`Form: fields \"${name}\", \"where\" compares \"${name}\" with itself.`);\n else if (!columnNamed(table.columns, local))\n problems.push(`Form: fields \"${name}\", \"where\" names \"${local}\", which is not a column of ${table.name}.`);\n }\n if (!schema) return problems;\n const target = schema.tables.find((candidate) => candidate.id === column.references!.table);\n if (!target)\n return [\n ...problems,\n `Form: fields \"${name}\" refers to ${column.references.table}, which this catalog does not read, so its choices cannot be narrowed.`,\n ];\n for (const remote of Object.keys(where)) {\n if (!columnNamed(target.columns, remote))\n problems.push(`Form: fields \"${name}\", \"where\" names \"${remote}\", which is not a column of ${target.name}.`);\n }\n return problems;\n}\n\nfunction referencedTable(to: string | undefined, schema: DatabaseSchema | undefined): TableMeta | undefined {\n return to && schema\n ? (schema.tables.find((candidate) => candidate.id === to) ?? schema.tables.find((candidate) => candidate.name === to))\n : undefined;\n}\n\n/** The table a reference column of `table` names, where the catalog reads it. */\nexport function referenceTarget(via: string, table: TableMeta, schema: DatabaseSchema | undefined): TableMeta | undefined {\n const reference = columnNamed(table.columns, via);\n const fk = schema?.foreignKeys.find((key) => key.from.table === table.id && key.from.columns.length === 1 && key.from.columns[0] === via);\n return reference ? referencedTable(reference.references?.table ?? fk?.to.table, schema) : undefined;\n}\n\n/**\n * A value from a referenced record (`defaults: { labor_charge: { from: 'service_id.price' } }`): the first name\n * is a reference of this table, the second a column of the table it names, holding what this column holds.\n */\nexport function derivedSource(\n path: string,\n table: TableMeta,\n schema: DatabaseSchema | undefined,\n): { via: ColumnMeta; target: TableMeta; column: ColumnMeta; key: string } | { problem: string } {\n const [via, name] = path.split('.');\n const reference = columnNamed(table.columns, via);\n if (!reference) return { problem: `\"${table.name}\" has no column \"${via}\".` };\n const fk = schema?.foreignKeys.find((key) => key.from.table === table.id && key.from.columns.length === 1 && key.from.columns[0] === via);\n const to = reference.references?.table ?? fk?.to.table;\n const target = referencedTable(to, schema);\n if (!to) return { problem: `\"${via}\" is no reference, so it names no record to take a value from.` };\n if (!target) return { problem: `\"${via}\" refers to ${to}, which this catalog does not read.` };\n const column = columnNamed(target.columns, name);\n if (!column) return { problem: `${target.name} has no column \"${name}\".` };\n const key = reference.references?.column ?? fk?.to.columns[0] ?? target.columns.find((candidate) => candidate.primaryKey)?.name;\n if (!key) return { problem: `${target.name} has no key \"${via}\" could name.` };\n return { via: reference, target, column, key };\n}\n\nfunction derivedProblems(name: string, path: string, table: TableMeta, schema: DatabaseSchema | undefined): string[] {\n const column = columnNamed(table.columns, name);\n if (!column) return [`Form: \"${table.name}\" has no column \"${name}\".`];\n if (!schema) return [];\n const source = derivedSource(path, table, schema);\n if ('problem' in source) return [`Form: defaults \"${name}\" is from \"${path}\": ${source.problem}`];\n if (columnKind(source.column.type) !== columnKind(column.type))\n return [\n `Form: defaults \"${name}\" is from \"${path}\", which is ${source.column.type || 'untyped'}; \"${name}\" is ${column.type || 'untyped'}.`,\n ];\n return [];\n}\n\nfunction defaultWordProblems(name: string, column: ColumnMeta, value: FormScalar, context: AppFormContext): string[] {\n if (value === 'today' || value === 'now') {\n return formDefaultWord(column, value)\n ? []\n : [\n `Form: defaults \"${name}\" is ${value}, which fills a date column; \"${name}\" is ${column.type || 'untyped'}` +\n \" (a SQLite TEXT date is declared with types: { column: 'date' }).\",\n ];\n }\n if (value !== 'me') return [];\n if (!formDefaultWord(column, value))\n return [\n `Form: defaults \"${name}\" is me, the signed-in person — a row of the auth.me table or an email; \"${name}\" is ${column.type || 'untyped'}.`,\n ];\n if (context.person === null) return [`Form: defaults \"${name}\" is me, and this App has no sign-in.`];\n if (context.person && column.references && column.references.table !== context.person.table) {\n return [\n `Form: defaults \"${name}\" is me, and \"${name}\" refers to ${column.references.table}, not to the table people are rows of (auth.me).`,\n ];\n }\n return [];\n}\n\nfunction tableId(table: TableMeta): string {\n return table.id;\n}\n\n/** Validate the child-group contract against a catalog before it is exposed.\n * This checks only shape, relation and field authority; it never creates a\n * child, infers a write grant, or treats a presentation form as permission. */\nexport function validateFormChildren(\n children: readonly FormChildGroup[],\n parent: TableMeta,\n schema: DatabaseSchema,\n readableFields: ReadonlyMap<string, readonly string[]> = new Map(),\n): string[] {\n const problems: string[] = [];\n const seen = new Set<string>();\n for (const child of parseFormChildren(children)) {\n if (seen.has(child.id)) problems.push(`Child group \"${child.id}\" is declared more than once.`);\n seen.add(child.id);\n const targets = schema.tables.filter((table) => table.id === child.table || table.name === child.table);\n const target = targets.length === 1 ? targets[0] : undefined;\n if (!target) {\n problems.push(`Child group \"${child.id}\" names an unknown or ambiguous table.`);\n continue;\n }\n const from = columnNamed(target.columns, child.relation.from);\n const to = columnNamed(parent.columns, child.relation.to);\n if (!from || !to) problems.push(`Child group \"${child.id}\" has an unknown relation column.`);\n const relation = schema.foreignKeys.filter(\n (fk) =>\n fk.from.table === tableId(target) &&\n fk.to.table === tableId(parent) &&\n fk.from.columns.length === 1 &&\n fk.to.columns.length === 1 &&\n fk.from.columns[0] === child.relation.from &&\n fk.to.columns[0] === child.relation.to,\n );\n if (relation.length !== 1) problems.push(`Child group \"${child.id}\" must name one declared foreign-key relation to its parent.`);\n const readable = readableFields.get(target.id) ?? target.columns.map((column) => column.name);\n if (child.form)\n problems.push(...validateAppForm(child.form, target, readable).map((problem) => `Child group \"${child.id}\": ${problem}`));\n }\n return [...new Set(problems)];\n}\n\ninterface FormChildInput {\n /** Child rows keyed by the authored group ID; absent means an empty draft. */\n rows?: readonly Readonly<Record<string, unknown>>[];\n}\n\ninterface FormChildrenResult {\n valid: boolean;\n errors: Record<string, string[]>;\n unresolved: string[];\n}\n\nfunction safeScalar(raw: unknown): FormScalar | undefined {\n if (raw === null) return null;\n if (typeof raw === 'string') return raw;\n if (typeof raw === 'boolean') return raw;\n if (typeof raw === 'number' && Number.isFinite(raw) && Math.abs(raw) <= Number.MAX_SAFE_INTEGER) return raw;\n return undefined;\n}\n\n/** Evaluate a bounded aggregate draft without persistence. The caller supplies\n * the already-authorized resource rules; unknown rows and relations remain\n * unresolved rather than becoming a false success. */\nexport function evaluateFormChildren(\n children: readonly FormChildGroup[],\n parentRow: Readonly<Record<string, unknown>>,\n inputs: Readonly<Record<string, FormChildInput | undefined>>,\n schema: DatabaseSchema,\n validation: ReadonlyMap<string, readonly FormRule[]> = new Map(),\n): FormChildrenResult {\n const errors: Record<string, string[]> = {};\n const unresolved = new Set<string>();\n const add = (key: string, message: string) => {\n const list = errors[key] ?? (errors[key] = []);\n if (!list.includes(message)) list.push(message);\n };\n for (const child of parseFormChildren(children)) {\n const targets = schema.tables.filter((table) => table.id === child.table || table.name === child.table);\n const target = targets.length === 1 ? targets[0] : undefined;\n if (!target) {\n unresolved.add(child.id);\n continue;\n }\n const input = inputs[child.id];\n const rows = input?.rows ?? [];\n if (!Array.isArray(rows) || rows.length > child.max || rows.length < (child.min ?? 0)) {\n add(child.id, `Add between ${child.min ?? 0} and ${child.max} items.`);\n continue;\n }\n const parentValue = safeScalar(rowValue(parentRow, child.relation.to));\n const fk = columnNamed(target.columns, child.relation.from);\n if (!fk || parentValue === undefined) {\n unresolved.add(child.id);\n continue;\n }\n const rules = validation.get(target.id) ?? validation.get(target.name) ?? [];\n rows.forEach((row, index) => {\n const key = `${child.id}[${index}]`;\n if (!row || typeof row !== 'object' || Array.isArray(row)) {\n unresolved.add(key);\n return;\n }\n const childValue = rowValue(row, child.relation.from);\n const linked = comparisonResult({ field: child.relation.from, operator: 'eq', values: [parentValue] }, row, target.columns);\n if (linked === false) add(`${key}.${child.relation.from}`, 'This item belongs to a different parent.');\n else if (linked === 'unknown' || childValue === undefined) unresolved.add(`${key}.${child.relation.from}`);\n if (rules.length) {\n const result = evaluateFormRules(rules, row, target.columns, { mode: 'authoritative' });\n for (const [fieldName, messages] of Object.entries(result.errors)) {\n for (const message of messages) add(`${key}.${fieldName}`, message);\n }\n result.unresolved.forEach((fieldName) => unresolved.add(`${key}.${fieldName}`));\n }\n });\n }\n return { valid: !Object.keys(errors).length && !unresolved.size, errors, unresolved: [...unresolved] };\n}\n\ninterface FormRuleResult {\n valid: boolean;\n errors: Record<string, string[]>;\n /** Rule target fields whose result is unknown, including incomplete drafts. */\n unresolved: string[];\n}\n\n/** Validate a complete candidate row, after trusted default resolution / update\n * merging. Does not read defaults, merge drafts, write, or grant authority.\n * Preview leaves unknown rules unresolved without showing premature field errors.\n * Authoritative mode rejects them with fixed safe messages. Both return valid:false.\n * Transport owners must also redact messages/field keys for unreadable targets.\n */\nexport function evaluateFormRules(\n rules: readonly FormRule[],\n row: Readonly<Record<string, unknown>>,\n columns: readonly ColumnMeta[],\n options?: { mode: 'authoritative' | 'preview' },\n): FormRuleResult {\n const errors: Record<string, string[]> = {};\n const unresolved = new Set<string>();\n const add = (name: string, message: string) => {\n const messages = errors[name] ?? (errors[name] = []);\n if (!messages.includes(message)) messages.push(message);\n };\n const judged = blanksAsEmpty(row, columns);\n for (const rule of parseFormRules(rules)) {\n const applies = rule.when ? evaluateFormCondition(rule.when, row, columns, true) : true;\n if (applies === false) continue;\n const at = ruleField(rule);\n let result: boolean | 'unknown' = 'unknown';\n if (applies === true) {\n try {\n result = conditionResult(compileCondition(rule.must, 'Validation', columns, 'record', true), judged, columns);\n } catch {\n result = 'unknown';\n }\n }\n if (result === 'unknown') {\n unresolved.add(at);\n if (options?.mode !== 'preview') add(at, FORM_UNRESOLVED_MESSAGE);\n } else if (!result) add(at, rule.message);\n }\n return { valid: !Object.keys(errors).length && !unresolved.size, errors, unresolved: [...unresolved] };\n}\n","/**\n * Flows whose steps are rows: a resource's `flows` binds a table of\n * flows and a table of their steps, which an administrator edits while the\n * App runs, to the durable workflow engine that runs them on its records:\n *\n * purchase_order: {\n * flows: {\n * from: 'po_flow', steps: 'po_flow_step',\n * set: ['notes'],\n * events: { 'po.decided': 'order' },\n * kinds: { approval: { command: 'requestApproval', input: ['approver'], waitFor: 'po.decided' } },\n * },\n * }\n *\n * The rows have fixed column names, so there is one way to write one. A flow\n * is `id`, `name` and `start` (`created`, `updated` or `manual`), and may\n * have a `condition` in the query language, `changes` (the one column an\n * update must change) and `active`. A step is `flow_id`, `position` and\n * `kind`, with the inputs its kind reads: the platform's own kinds (`wait`,\n * `until`, `branch`, `set`, `waitFor`, `notify`) and the App's, each of which runs one\n * of the App's commands on the record — so whatever a flow does to data goes\n * through the command door, under its rules, principal and audit.\n *\n * Steps that share a position start together, and the run goes on once each\n * is done. A step of an App kind that waits for an event may wait for `all`\n * of those its command asked (its `mode`), end early when its `condition`\n * holds, give up after its `duration`, and run the kind's `remind` command\n * every `remind` while it waits.\n *\n * A `notify` step tells an `audience` its `message`: the resource's `notify`\n * rules `on: { flow: '<audience>' }` say who that is and how (`to`, `send`,\n * `email`), and the words are the step's (`{message}` in the rule's title),\n * kept in the inbox in the step's own transaction (adapters/inbox/inbox.ts).\n *\n * Pure, and shared: the server judges a stored row here (appValidation.ts)\n * and compiles a flow's rows into the plan a run follows (flowCommand.ts)\n * with the same words, so a row the form accepted is a step the run can take.\n */\nimport { parseDuration } from '@mistriwork/engine/shared/duration.js';\n\n/** A step kind the App declares: the command it runs on the record, the step columns it takes as input, and an event to wait for after. */\ninterface FlowKind {\n readonly command: string;\n /** Step columns passed to the command under their own names; `'note?'` is one a step may leave empty. */\n readonly input?: readonly string[];\n /** An event of `events` the run waits for once the command has run. */\n readonly waitFor?: string;\n /** A command run on the record, with the same input, every `remind` a step of this kind waits. */\n readonly remind?: string;\n}\n\n/** `resources.<r>.flows`: the tables the flows and their steps are rows of, and what those rows may do. */\nexport interface FlowBinding {\n readonly from: string;\n readonly steps: string;\n /** Columns of the record a `set` step may write. None: the App offers no `set` step. */\n readonly set?: readonly string[];\n /** The events a run may wait for, each with the payload field that names the record it is about. */\n readonly events?: Readonly<Record<string, string>>;\n readonly kinds?: Readonly<Record<string, FlowKind>>;\n}\n\n/** The platform's own step kinds. */\nconst BUILT_IN_KINDS = ['wait', 'until', 'branch', 'set', 'waitFor', 'notify'] as const;\n/** The longest a notify step's words may be: a notice is a line. */\nconst MESSAGE_MOST = 500;\nconst FLOW_TRIGGERS = ['created', 'updated', 'manual'] as const;\nexport type FlowTrigger = (typeof FLOW_TRIGGERS)[number];\n\n/** The columns every flows table has, and every steps table: the rest are read where present. */\nconst FLOW_NEEDED = ['id', 'name', 'start'] as const;\nconst STEP_NEEDED = ['flow_id', 'position', 'kind'] as const;\n/** The most steps one flow runs: a process, not a program. */\nexport const FLOW_STEPS_MOST = 100;\n/** The longest a step may wait: the workflow engine's own ceiling. */\nconst DURATION_MOST = 366 * 86_400_000;\n\nconst IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;\nconst KIND_NAME = /^[A-Za-z][A-Za-z0-9_]{0,39}$/;\nconst EVENT_NAME = /^[A-Za-z][A-Za-z0-9_.-]{0,127}$/;\nconst COMMAND_ID = /^[A-Za-z][A-Za-z0-9_-]*$/;\n\nconst isObject = (value: unknown): value is Record<string, unknown> => !!value && typeof value === 'object' && !Array.isArray(value);\n\n/** `resources.<r>.flows`, read for its shape. Throws the sentence the author reads. */\nexport function parseFlows(raw: unknown, at: string): FlowBinding {\n const here = `${at}, flows`;\n if (!isObject(raw))\n throw new Error(\n `${here} is not an object → { from, steps, set?, events?, kinds? }, like flows: { from: 'po_flow', steps: 'po_flow_step' }.`,\n );\n const unknown = Object.keys(raw).find((key) => !['from', 'steps', 'set', 'events', 'kinds'].includes(key));\n if (unknown) throw new Error(`${here} has \"${unknown}\"; it takes from, steps, set, events and kinds.`);\n for (const name of ['from', 'steps'] as const)\n if (typeof raw[name] !== 'string' || !raw[name].trim())\n throw new Error(`${here}: \"${name}\" names the resource whose rows are the ${name === 'from' ? 'flows' : \"flows' steps\"}.`);\n const binding: { -readonly [K in keyof FlowBinding]: FlowBinding[K] } = {\n from: (raw.from as string).trim(),\n steps: (raw.steps as string).trim(),\n };\n if (raw.set !== undefined) {\n if (!Array.isArray(raw.set) || !raw.set.length || raw.set.some((column) => typeof column !== 'string' || !IDENTIFIER.test(column)))\n throw new Error(`${here}: \"set\" lists the record's own columns a set step may write, like set: ['notes'].`);\n binding.set = Object.freeze([...new Set(raw.set as string[])]);\n }\n if (raw.events !== undefined) {\n if (!isObject(raw.events) || !Object.keys(raw.events).length)\n throw new Error(\n `${here}: \"events\" names each event a run may wait for and the payload field naming the record, like events: { 'po.decided': 'order' }.`,\n );\n for (const [name, field] of Object.entries(raw.events)) {\n if (!EVENT_NAME.test(name))\n throw new Error(`${here}, events \"${name}\": an event is named as defineEvent names it, like 'po.decided'.`);\n if (typeof field !== 'string' || !IDENTIFIER.test(field))\n throw new Error(`${here}, events \"${name}\": name the payload field that holds the record's id, like 'order'.`);\n }\n binding.events = Object.freeze({ ...(raw.events as Record<string, string>) });\n }\n if (raw.kinds !== undefined) {\n if (!isObject(raw.kinds) || !Object.keys(raw.kinds).length)\n throw new Error(`${here}: \"kinds\" names each step kind the App adds, like kinds: { send: { command: 'sendPurchaseOrder' } }.`);\n const kinds: Record<string, FlowKind> = {};\n for (const [name, value] of Object.entries(raw.kinds)) {\n const kind = `${here}, kinds \"${name}\"`;\n if (!KIND_NAME.test(name)) throw new Error(`${kind}: a step kind is a word of letters, digits and underscores.`);\n if ((BUILT_IN_KINDS as readonly string[]).includes(name))\n throw new Error(\n `${kind}: \"${name}\" is one of the platform's own step kinds (${BUILT_IN_KINDS.join(', ')}); name the App's kind another word.`,\n );\n if (!isObject(value)) throw new Error(`${kind} is not an object → { command, input?, waitFor?, remind? }.`);\n const extra = Object.keys(value).find((key) => !['command', 'input', 'waitFor', 'remind'].includes(key));\n if (extra) throw new Error(`${kind} has \"${extra}\"; it takes command, input, waitFor and remind.`);\n if (typeof value.command !== 'string' || !COMMAND_ID.test(value.command))\n throw new Error(`${kind}: \"command\" names one of the App's commands, which the step runs on the record.`);\n const entry: { command: string; input?: readonly string[]; waitFor?: string; remind?: string } = { command: value.command };\n if (value.input !== undefined) {\n if (\n !Array.isArray(value.input) ||\n !value.input.length ||\n value.input.some((column) => typeof column !== 'string' || !IDENTIFIER.test(column.replace(/\\?$/, '')))\n )\n throw new Error(\n `${kind}: \"input\" lists the step columns the command takes, like input: ['approver'] ('note?' for one a step may leave empty).`,\n );\n entry.input = Object.freeze([...(value.input as string[])]);\n }\n if (value.waitFor !== undefined) {\n if (typeof value.waitFor !== 'string' || !binding.events || !Object.hasOwn(binding.events, value.waitFor))\n throw new Error(`${kind}: \"waitFor\" names one of the flows' events, which \"events\" lists.`);\n entry.waitFor = value.waitFor;\n }\n if (value.remind !== undefined) {\n if (typeof value.remind !== 'string' || !COMMAND_ID.test(value.remind) || entry.waitFor === undefined)\n throw new Error(`${kind}: \"remind\" names one of the App's commands, run on the record while a step waits for \"waitFor\".`);\n entry.remind = value.remind;\n }\n kinds[name] = Object.freeze(entry);\n }\n binding.kinds = Object.freeze(kinds);\n }\n return Object.freeze(binding);\n}\n\n/** Every command a binding's kinds run, with the kind that runs it: its step's command, and its reminder. */\nexport const flowKindCommands = (binding: FlowBinding): { kind: string; command: string }[] =>\n Object.entries(binding.kinds ?? {}).flatMap(([kind, declared]) => [\n { kind, command: declared.command },\n ...(declared.remind ? [{ kind, command: declared.remind }] : []),\n ]);\n\n/** A kind's input column, and whether a step may leave it empty. */\nconst kindInputs = (kind: FlowKind): { column: string; optional: boolean }[] =>\n (kind.input ?? []).map((name) => ({ column: name.replace(/\\?$/, ''), optional: name.endsWith('?') }));\n\ninterface CatalogTable {\n readonly name: string;\n readonly columns: readonly { readonly name: string; readonly primaryKey: boolean }[];\n}\n\n/** Why a binding cannot run on this catalog, or undefined. `machine` is the record's state column, which only its verbs move. */\nexport function flowsProblem(\n binding: FlowBinding,\n table: CatalogTable,\n from: CatalogTable | undefined,\n steps: CatalogTable | undefined,\n machine?: string,\n): string | undefined {\n const has = (meta: CatalogTable, name: string) => meta.columns.some((column) => column.name === name);\n if (table.columns.filter((column) => column.primaryKey).length !== 1)\n return `flows: \"${table.name}\" has no single-column key; a flow's run and the events it waits for name a record by one key.`;\n if (!from) return `flows: \"${binding.from}\" is not a declared resource on this connection.`;\n if (!steps) return `flows: \"${binding.steps}\" is not a declared resource on this connection.`;\n const missing = (meta: CatalogTable, needed: readonly string[]) => needed.filter((name) => !has(meta, name));\n const flowGap = missing(from, FLOW_NEEDED);\n if (flowGap.length)\n return `flows: \"${from.name}\" has no ${flowGap.map((name) => `\"${name}\"`).join(', ')}; a flow row has id, name and start (and may have condition, changes and active).`;\n const stepGap = missing(steps, STEP_NEEDED);\n if (stepGap.length)\n return `flows: \"${steps.name}\" has no ${stepGap.map((name) => `\"${name}\"`).join(', ')}; a step row has flow_id, position and kind (and the columns its kind reads).`;\n for (const column of binding.set ?? []) {\n if (!has(table, column)) return `flows: set names \"${column}\", which \"${table.name}\" does not have.`;\n if (table.columns.find((candidate) => candidate.name === column)!.primaryKey) return `flows: set names \"${column}\", the record's key.`;\n if (column === machine) return `flows: set names \"${column}\", the state column of the record's machine, which only its verbs move.`;\n }\n for (const [name, kind] of Object.entries(binding.kinds ?? {}))\n for (const { column } of kindInputs(kind))\n if (!has(steps, column)) return `flows: kind \"${name}\" takes \"${column}\", which \"${steps.name}\" does not have.`;\n return undefined;\n}\n\n/* ---------- a row, judged ---------- */\n\n/** What a row breaks, by the column that says it. */\ntype FlowBreak =\n | 'name'\n | 'start'\n | 'condition'\n | 'changes'\n | 'kind'\n | 'position'\n | 'duration'\n | 'dateField'\n | 'setField'\n | 'goto'\n | 'event'\n | 'input'\n | 'mode'\n | 'remind'\n | 'together'\n | 'audience'\n | 'message'\n | 'flowId';\n\n/** What the server says of a break: a fixed sentence, never the row's value. */\nexport const FLOW_REASONS: Readonly<Record<FlowBreak, string>> = Object.freeze({\n name: 'Name the flow.',\n start: 'Choose created, updated or manual.',\n condition: 'Write a condition in the query language about this record, like status = draft.',\n changes: \"Name one of the record's columns.\",\n kind: 'Choose one of the step kinds this App offers.',\n position: 'Give the step a whole-number position.',\n duration: \"Enter a duration, like '2 hours' or '3 days', at most a year.\",\n dateField: \"Name one of the record's date columns.\",\n setField: 'Name a column a flow may set.',\n goto: \"Name a later step's position.\",\n event: 'Choose one of the events this App’s flows wait for.',\n input: 'This step kind needs a value here.',\n mode: 'Choose any or all: all waits for each one the step asks.',\n remind: \"Enter how often to remind, like '1 day', on a step whose kind reminds.\",\n together: 'A branch decides alone: give it a position no other step has.',\n audience: \"Choose who is told: one of the audiences this App's notices name.\",\n message: `Write what they are told, at most ${MESSAGE_MOST} characters.`,\n flowId: 'Choose the flow this step belongs to.',\n});\n\n/** What the judge knows of the record's table: its columns, its date columns, and whether a condition reads. */\nexport interface FlowRecordShape {\n readonly columns: ReadonlySet<string>;\n readonly dates: ReadonlySet<string>;\n /** Whether a condition is a filter over the record's stored columns. */\n readonly condition: (text: string) => boolean;\n /** The audiences the resource's `notify` rules name `on: { flow }`: whom a `notify` step may tell. */\n readonly audiences?: ReadonlySet<string>;\n}\n\nconst text = (value: unknown): string | undefined => {\n if (value === null || value === undefined) return undefined;\n const said = typeof value === 'string' ? value.trim() : typeof value === 'number' || typeof value === 'boolean' ? String(value) : '';\n return said ? said : undefined;\n};\nconst whole = (value: unknown): number | undefined => {\n const n = typeof value === 'number' ? value : typeof value === 'string' && /^\\s*-?\\d+\\s*$/.test(value) ? Number(value) : NaN;\n return Number.isSafeInteger(n) ? n : undefined;\n};\nconst durationMs = (value: unknown): number | undefined => {\n const ms = parseDuration(text(value));\n return ms && ms <= DURATION_MOST ? ms : undefined;\n};\n\n/** Every break of a flow row, by column. */\nexport function flowRowBreaks(row: Readonly<Record<string, unknown>>, record: FlowRecordShape): { field: string; reason: FlowBreak }[] {\n const breaks: { field: string; reason: FlowBreak }[] = [];\n if (!text(row.name)) breaks.push({ field: 'name', reason: 'name' });\n if (!(FLOW_TRIGGERS as readonly string[]).includes(text(row.start) ?? '')) breaks.push({ field: 'start', reason: 'start' });\n const condition = text(row.condition);\n if (condition !== undefined && !record.condition(condition)) breaks.push({ field: 'condition', reason: 'condition' });\n const changes = text(row.changes);\n if (changes !== undefined && !record.columns.has(changes)) breaks.push({ field: 'changes', reason: 'changes' });\n return breaks;\n}\n\n/** Every break of a step row, by column: an unknown kind, or an input its kind needs and lacks or cannot read. */\nexport function stepRowBreaks(\n row: Readonly<Record<string, unknown>>,\n binding: FlowBinding,\n record: FlowRecordShape,\n): { field: string; reason: FlowBreak }[] {\n const breaks: { field: string; reason: FlowBreak }[] = [];\n const say = (field: string, reason: FlowBreak) => breaks.push({ field, reason });\n if (row.flow_id === null || row.flow_id === undefined || row.flow_id === '') say('flow_id', 'flowId');\n const position = whole(row.position);\n if (position === undefined || position < 0) say('position', 'position');\n const kind = text(row.kind) ?? '';\n const duration = text(row.duration);\n switch (kind) {\n case 'wait':\n if (durationMs(duration) === undefined) say('duration', 'duration');\n break;\n case 'until':\n if (!record.dates.has(text(row.field) ?? '')) say('field', 'dateField');\n if (duration !== undefined && durationMs(duration) === undefined) say('duration', 'duration');\n break;\n case 'branch': {\n const condition = text(row.condition);\n if (condition === undefined || !record.condition(condition)) say('condition', 'condition');\n // A branch only jumps forward, so a run visits each step at most once.\n if (text(row.goto) !== undefined) {\n const to = whole(row.goto);\n if (to === undefined || (position !== undefined && to <= position)) say('goto', 'goto');\n }\n break;\n }\n case 'set':\n if (!(binding.set ?? []).includes(text(row.field) ?? '')) say('field', 'setField');\n break;\n case 'waitFor':\n if (!Object.hasOwn(binding.events ?? {}, text(row.event) ?? '')) say('event', 'event');\n if (duration !== undefined && durationMs(duration) === undefined) say('duration', 'duration');\n break;\n case 'notify': {\n if (!record.audiences?.has(text(row.audience) ?? '')) say('audience', 'audience');\n const message = text(row.message);\n if (message === undefined || message.length > MESSAGE_MOST) say('message', 'message');\n break;\n }\n default: {\n const declared = Object.hasOwn(binding.kinds ?? {}, kind) ? binding.kinds![kind] : undefined;\n if (!declared) {\n say('kind', 'kind');\n break;\n }\n const inputs = kindInputs(declared);\n for (const { column, optional } of inputs) if (!optional && text(row[column]) === undefined) say(column, 'input');\n /* The wait's own columns, where the kind does not take them as input: only a kind that waits has a wait to shape. */\n const own = (column: string) => !inputs.some((input) => input.column === column);\n const waits = declared.waitFor !== undefined;\n const mode = text(row.mode);\n if (own('mode') && mode !== undefined && (!waits || (mode !== 'any' && mode !== 'all'))) say('mode', 'mode');\n const remind = text(row.remind);\n if (own('remind') && remind !== undefined && (!declared.remind || durationMs(remind) === undefined)) say('remind', 'remind');\n if (own('duration') && duration !== undefined && (!waits || durationMs(duration) === undefined)) say('duration', 'duration');\n const condition = text(row.condition);\n if (own('condition') && condition !== undefined && (!waits || !record.condition(condition))) say('condition', 'condition');\n }\n }\n return breaks;\n}\n\n/* ---------- a flow's rows, as the plan a run follows ---------- */\n\n/** One step as a run takes it: read once, when the run starts, and kept with it. */\nexport type PlanStep = Readonly<\n {\n position: number;\n title?: string;\n /** Its place among the steps that share its position, from 1, where more than one does: they start together. */\n member?: number;\n } & (\n | { kind: 'wait'; duration: string }\n | { kind: 'until'; field: string; duration?: string }\n | { kind: 'branch'; condition: string; goto?: number }\n | { kind: 'set'; field: string; value: string | null }\n | { kind: 'waitFor'; event: string; field: string; duration?: string }\n | { kind: 'notify'; audience: string; message: string }\n | {\n kind: 'command';\n name: string;\n command: string;\n input: Readonly<Record<string, string>>;\n waitFor?: { event: string; field: string };\n /** Wait for each one the command asked (its output's `asked`), not the first. */\n mode?: 'all';\n /** The wait ends early once this holds of the record. */\n condition?: string;\n /** The most the wait lasts; then the run goes on. */\n duration?: string;\n /** The command run every `every` while the step waits. */\n remind?: { command: string; every: string };\n }\n )\n>;\n\nexport interface FlowPlan {\n readonly flow: string;\n readonly name: string;\n readonly steps: readonly PlanStep[];\n /** The first row that cannot run: the run stops on it, saying why, before it does anything. */\n readonly problem?: Readonly<{ position: number | null; reason: string }>;\n}\n\n/** A flow's rows as its plan, in position order. Judged as a write judges them, so a row the form refused never runs. */\nexport function flowPlan(\n flow: Readonly<Record<string, unknown>>,\n rows: readonly Readonly<Record<string, unknown>>[],\n binding: FlowBinding,\n record: FlowRecordShape,\n): FlowPlan {\n const id = text(flow.id) ?? '';\n const name = text(flow.name) ?? id;\n /* Steps that share a position keep one order, by their own key, so a run's steps are named the same each time they are read. */\n const ordered = [...rows].sort(\n (a, b) => (whole(a.position) ?? 0) - (whole(b.position) ?? 0) || String(a.id ?? '').localeCompare(String(b.id ?? '')),\n );\n if (ordered.length > FLOW_STEPS_MOST)\n return { flow: id, name, steps: [], problem: { position: null, reason: `A flow runs at most ${FLOW_STEPS_MOST} steps.` } };\n const steps: PlanStep[] = [];\n const shared = new Map<number, number>();\n for (const row of ordered) shared.set(whole(row.position) ?? -1, (shared.get(whole(row.position) ?? -1) ?? 0) + 1);\n const members = new Map<number, number>();\n for (const row of ordered) {\n const position = whole(row.position) ?? -1;\n const broken = stepRowBreaks(row, binding, record)[0];\n if (broken) return { flow: id, name, steps: [], problem: { position, reason: `${broken.field}: ${FLOW_REASONS[broken.reason]}` } };\n const together = shared.get(position)! > 1;\n if (together && (text(row.kind) === 'branch' || text(row.remind) !== undefined))\n return {\n flow: id,\n name,\n steps: [],\n problem: {\n position,\n reason: text(row.kind) === 'branch' ? FLOW_REASONS.together : 'A step that starts together with others sends no reminders.',\n },\n };\n const member = (members.get(position) ?? 0) + 1;\n members.set(position, member);\n const title = text(row.title);\n const head = { position, ...(title ? { title: title.slice(0, 200) } : {}), ...(together ? { member } : {}) };\n const kind = text(row.kind)!;\n const duration = text(row.duration);\n switch (kind) {\n case 'wait':\n steps.push({ ...head, kind, duration: duration! });\n break;\n case 'until':\n steps.push({ ...head, kind, field: text(row.field)!, ...(duration ? { duration } : {}) });\n break;\n case 'branch': {\n const to = whole(row.goto);\n steps.push({ ...head, kind, condition: text(row.condition)!, ...(to === undefined ? {} : { goto: to }) });\n break;\n }\n case 'set':\n steps.push({ ...head, kind, field: text(row.field)!, value: text(row.value) ?? null });\n break;\n case 'waitFor': {\n const event = text(row.event)!;\n steps.push({ ...head, kind, event, field: binding.events![event], ...(duration ? { duration } : {}) });\n break;\n }\n case 'notify':\n steps.push({ ...head, kind, audience: text(row.audience)!, message: text(row.message)! });\n break;\n default: {\n const declared = binding.kinds![kind];\n const inputs = kindInputs(declared);\n const input = Object.fromEntries(\n inputs.flatMap(({ column }) => {\n const value = text(row[column]);\n return value === undefined ? [] : [[column, value]];\n }),\n );\n const own = (column: string) => (inputs.some((each) => each.column === column) ? undefined : text(row[column]));\n const wait = declared.waitFor\n ? {\n waitFor: { event: declared.waitFor, field: binding.events![declared.waitFor] },\n ...(own('mode') === 'all' ? { mode: 'all' as const } : {}),\n ...(own('condition') ? { condition: own('condition')! } : {}),\n ...(own('duration') ? { duration: own('duration')! } : {}),\n ...(declared.remind && own('remind') ? { remind: { command: declared.remind, every: own('remind')! } } : {}),\n }\n : {};\n steps.push({ ...head, kind: 'command', name: kind, command: declared.command, input, ...wait });\n }\n }\n }\n return { flow: id, name, steps };\n}\n\n/** Whether a flow row is switched on: no `active` column, or one that is not false. */\nexport function flowActive(row: Readonly<Record<string, unknown>>): boolean {\n const value = row.active;\n return !(value === false || value === 0 || value === '0' || value === 'false' || value === 'f');\n}\n\n/** Whether a flow starts on this change: its `start`, and an update's `changes` column among the columns that changed.\n * A change naming a flow starts that one alone: a manual start, or `mistri flows start --flow` (flowStarts.ts). */\nexport function flowTriggered(\n row: Readonly<Record<string, unknown>>,\n change: Readonly<{ trigger: FlowTrigger; changed: readonly string[]; flow?: string }>,\n): boolean {\n if (!flowActive(row)) return false;\n const trigger = text(row.start);\n if (change.trigger === 'manual') return trigger === 'manual' && text(row.id) === change.flow;\n if (trigger !== change.trigger || (change.flow !== undefined && text(row.id) !== change.flow)) return false;\n const changes = text(row.changes);\n return change.trigger === 'created' || changes === undefined || change.changed.includes(changes);\n}\n","/**\n * The dashboard sections that are not about one table's figures — a new\n * App's first run, an author's own component with its declared reads, the\n * activity feed across resources — and a section's own time windows.\n * Parsed here, beside the dashboard parser that calls them (appViews.ts).\n */\nimport { ACTIVITY_MAX, FIRST_RUN_MAX_STEPS, SECTION_KEYS, type AppActivityResource, type AppDashSection } from './appModel.js';\nimport { allowKeys, isRecord, optionalText } from '@mistriwork/engine/shared/model.js';\nimport { parseIcon } from '@mistriwork/engine/shared/appIcons.js';\nimport { slug } from '@mistriwork/engine/shared/slug.js';\nimport { WIDTHS, type SectionWidth } from '@mistriwork/engine/shared/page.js';\nimport { checkDeclaredResource, checkResourceSlot, type ParsedData } from './appResourceBlocks.js';\nimport { componentPropValues, parseComponentDecl, parseDashReads, parseDashTotals } from '@mistriwork/engine/shared/appComponents.js';\n\n/** A new App's first steps, each a list view whose New form it opens: drawn until every step's\n table has a row, and alone while none has — next actions, not charts of nothing (Dashboard.tsx). */\n/* The sentence under a heading is `description` everywhere (renamed before 1.0). */\nconst FIRST_RUN_RENAMED = { text: 'is now \"description\": the sentence under the title' } as const;\n/* A destination's words are its `label` (renamed before 1.0). */\nconst FIRST_RUN_STEP_RENAMED = { ...FIRST_RUN_RENAMED, title: 'is now \"label\": the next action' } as const;\n\nexport function firstRunSection(\n s: Record<string, unknown>,\n at: string,\n views: ReadonlyMap<string, { kind: string; key: string }>,\n): AppDashSection {\n allowKeys(s, SECTION_KEYS.firstRun, `${at} (a first run)`, FIRST_RUN_RENAMED);\n const title = optionalText(s, 'title', at);\n const text = optionalText(s, 'description', at);\n const listViews = [...views.values()].filter((view) => view.kind === 'list').map((view) => view.key);\n if (!Array.isArray(s.steps) || !s.steps.length || s.steps.length > FIRST_RUN_MAX_STEPS) {\n throw new Error(\n `${at} is a first run and needs \"steps\": 1 to ${FIRST_RUN_MAX_STEPS} list views to start, like { label: 'Add your first customer', to: 'customers' }.`,\n );\n }\n const steps = s.steps.map((raw, i) => {\n const here = `${at}, step ${i + 1}`;\n if (!isRecord(raw)) throw new Error(`${here} is not an object with \"label\" and \"to\".`);\n allowKeys(raw, ['label', 'description', 'to'], here, FIRST_RUN_STEP_RENAMED);\n const label = optionalText(raw, 'label', here);\n if (!label) throw new Error(`${here} needs a \"label\": the next action, like \"Add your first customer\".`);\n const target = typeof raw.to === 'string' ? views.get(raw.to.trim()) : undefined;\n if (!target || target.kind !== 'list') {\n throw new Error(\n `${here} opens \"${String(raw.to)}\", which is not a list view this App declares. Its list views: ${listViews.join(', ') || 'none'}.`,\n );\n }\n const detail = optionalText(raw, 'description', here);\n return { label, view: target.key, ...(detail ? { detail } : {}) };\n });\n const repeated = steps.find((step, i) => steps.findIndex((other) => other.view === step.view) !== i);\n if (repeated)\n throw new Error(\n `${at} names the view \"${repeated.view}\" in two steps. A step is done when its list has a row, so each view is one step.`,\n );\n return { kind: 'firstRun', ...(title ? { title } : {}), ...(text ? { text } : {}), steps };\n}\n\n/**\n * An author's component on a dashboard: its reads are rows sections in all\n * but drawing — each names its resource, narrows by a query, and may say the\n * `over` the reader's window narrows it by — so the catalog check, the window\n * and the pick filters reach them as they reach every section.\n */\nexport function parseCustomDashSection(s: Record<string, unknown>, at: string, data: ParsedData): AppDashSection {\n const component = parseComponentDecl(s.component, `${at}, \"component\"`, 'section');\n const title = optionalText(s, 'title', at);\n if (s.width !== undefined && !WIDTHS.includes(String(s.width) as SectionWidth)) {\n throw new Error(`${at} has a \"width\" of \"${String(s.width)}\". Use ${WIDTHS.join(', ')}.`);\n }\n const reads = parseDashReads(s.rows, at);\n for (const [name, read] of Object.entries(reads ?? {})) {\n checkResourceSlot(read.table, `${at}, rows \"${name}\", \"from\"`, data);\n checkDeclaredResource(read.table, `${at}, rows \"${name}\", \"from\"`, data);\n }\n const totals = parseDashTotals(s.figures, at);\n for (const [name, total] of Object.entries(totals ?? {})) {\n checkResourceSlot(total.table, `${at}, figures \"${name}\", \"from\"`, data);\n checkDeclaredResource(total.table, `${at}, figures \"${name}\", \"from\"`, data);\n }\n return {\n kind: 'custom',\n component,\n ...(title === undefined ? {} : { title }),\n ...(reads ? { reads } : {}),\n ...(totals ? { aggregates: totals } : {}),\n props: componentPropValues(component, s.props, at),\n ...(s.width === undefined ? {} : { width: s.width as SectionWidth }),\n };\n}\n\n/**\n * A section's own windows: the reader picks this section's period in its\n * heading, apart from the dashboard's window, the first phrase where it\n * starts. They narrow by the section's `over`, as the dashboard's do, so a\n * section without one is refused rather than given a picker that moves\n * nothing. Each phrase is judged against the catalog with the rest of the\n * section (app.ts, resolveDashboard).\n */\nexport function parseSectionWindows(s: Record<string, unknown>, at: string): string[] | undefined {\n if (s.windows === undefined) return undefined;\n if (!Array.isArray(s.windows) || !s.windows.length || s.windows.length > 8 || s.windows.some((w) => typeof w !== 'string' || !w.trim())) {\n throw new Error(`${at} has \"windows\" that are not a list of one to eight phrases like \"last 7 days\".`);\n }\n const windows = (s.windows as string[]).map((w) => w.trim().replace(/\\s+/g, ' '));\n if (new Set(windows.map((w) => w.toLowerCase())).size !== windows.length) throw new Error(`${at} names one of its \"windows\" twice.`);\n if (typeof s.over !== 'string' || !s.over.trim()) {\n throw new Error(`${at} has \"windows\" and no \"over\": name the date column its period narrows, like over: 'moved_at'.`);\n }\n return windows;\n}\n\n/**\n * An embed that works as its room does — its search and filters, and its\n * selection with the bulk actions its room offers the reader — rather than\n * read-only. Opt-in, since most dashboard lists are a glance. The aside draws\n * a list as names, not a table, so there is no room there for its controls,\n * and a board or a calendar is drawn by the dashboard as a board or not at all.\n */\nexport function embedInteractive(\n s: Record<string, unknown>,\n at: string,\n view: string,\n presentation: string | undefined,\n aside: boolean,\n): true | undefined {\n if (s.interactive === undefined || s.interactive === false) return undefined;\n if (s.interactive !== true) throw new Error(`${at} has an \"interactive\" that is not true or false.`);\n if (aside) throw new Error(`${at} embeds \"${view}\" as interactive in the aside, which draws a list as names. Move it into \"sections\".`);\n if (presentation === 'board' || presentation === 'calendar') {\n throw new Error(\n `${at} embeds \"${view}\", a ${presentation}, as interactive; a dashboard draws a ${presentation} read-only. Open its room to work it.`,\n );\n }\n return true;\n}\n\n/**\n * Each section with windows of its own is named in the address by its title\n * (`?period.lines-moved-by-day=last-7-days`), or by its place (`section-3`) where the\n * title has no letters or digits: a name no other windowed\n * section on the dashboard has, and never an empty one.\n */\nexport function nameSectionWindows(sections: AppDashSection[], at: string): void {\n const seen = new Map<string, string>();\n for (const [index, section] of sections.entries()) {\n if (!section.windows) continue;\n const title = section.title ?? '';\n /* A title with no Latin letters or digits (日本語, a reader's words) slugs to nothing: the section's place names it\n instead, so the key is stable in every language and never empty. */\n const key = slug(title, '') || `section-${index + 1}`;\n const other = seen.get(key);\n if (other !== undefined) {\n throw new Error(\n `${at}: \"${other}\" and \"${title}\" both have their own \"windows\" and read as \"${key}\" in the address. Give one another title.`,\n );\n }\n seen.set(key, title);\n section.windowKey = key;\n }\n}\n\n/** How many resources one find box searches: each is a question per keystroke pause. */\nconst FIND_MAX = 6;\n\n/**\n * One box over several resources' records, by their names: each resource\n * named is one the App declares, searched as the reader reads it.\n */\nexport function parseFindSection(s: Record<string, unknown>, at: string, data: ParsedData): AppDashSection {\n const title = optionalText(s, 'title', at);\n const placeholder = optionalText(s, 'placeholder', at);\n if (\n !Array.isArray(s.resources) ||\n !s.resources.length ||\n s.resources.length > FIND_MAX ||\n s.resources.some((name) => typeof name !== 'string')\n ) {\n throw new Error(\n `${at} is a find and needs \"resources\": one to ${FIND_MAX} resource names whose records it searches, like ['article', 'catalog_item'].`,\n );\n }\n const searched = [...new Set(s.resources as string[])];\n for (const name of searched) {\n checkResourceSlot(name, `${at}, resources \"${name}\"`, data);\n checkDeclaredResource(name, `${at}, resources \"${name}\"`, data);\n }\n if (s.width !== undefined && !WIDTHS.includes(String(s.width) as SectionWidth)) {\n throw new Error(`${at} has a \"width\" of \"${String(s.width)}\". Use ${WIDTHS.join(', ')}.`);\n }\n return {\n kind: 'find',\n searched,\n ...(title === undefined ? {} : { title }),\n ...(placeholder === undefined ? {} : { placeholder }),\n ...(s.icon === undefined ? {} : { icon: parseIcon(s.icon, at) }),\n ...(s.width === undefined ? {} : { width: s.width as SectionWidth }),\n };\n}\n\n/**\n * Recent changes across resources, read from their record history: each\n * resource named is one the App declares, carries at most a mark and its word\n * in the feed, and keeps a history from then on (server/recordHistory.ts),\n * as a page's `history()` makes its resource keep one.\n */\nexport function parseActivitySection(s: Record<string, unknown>, at: string, data: ParsedData): AppDashSection {\n const title = optionalText(s, 'title', at);\n if (!isRecord(s.resources) || !Object.keys(s.resources).length || Object.keys(s.resources).length > 12) {\n throw new Error(\n `${at} is an activity and needs \"resources\": one to twelve resources whose changes it lists, like { pick_list: { icon: 'truck' } }.`,\n );\n }\n const resources: Record<string, AppActivityResource> = {};\n for (const [name, raw] of Object.entries(s.resources)) {\n const here = `${at}, resources \"${name}\"`;\n checkResourceSlot(name, here, data);\n checkDeclaredResource(name, here, data);\n if (!isRecord(raw)) throw new Error(`${here} is not an object: { icon?, label? }.`);\n allowKeys(raw, ['icon', 'label'], here);\n const label = optionalText(raw, 'label', here);\n resources[name] = { ...(raw.icon === undefined ? {} : { icon: parseIcon(raw.icon, here) }), ...(label ? { label } : {}) };\n }\n if (s.limit !== undefined && (typeof s.limit !== 'number' || !Number.isInteger(s.limit) || s.limit < 1 || s.limit > ACTIVITY_MAX)) {\n throw new Error(`${at} has a \"limit\" that is not a whole number from 1 to ${ACTIVITY_MAX}: the changes drawn before \"Show more\".`);\n }\n if (s.width !== undefined && !WIDTHS.includes(String(s.width) as SectionWidth)) {\n throw new Error(`${at} has a \"width\" of \"${String(s.width)}\". Use ${WIDTHS.join(', ')}.`);\n }\n return {\n kind: 'activity',\n resources,\n ...(title === undefined ? {} : { title }),\n ...(s.icon === undefined ? {} : { icon: parseIcon(s.icon, at) }),\n ...(s.limit === undefined ? {} : { limit: s.limit as number }),\n ...(s.width === undefined ? {} : { width: s.width as SectionWidth }),\n };\n}\n","/**\n * Content: everything a visitor can open, keyed in `views`, and the `nav`\n * that only arranges references to them. A view is one of:\n *\n * - `list`: the rows of one resource — how they read (`show`, `tabs`,\n * `filters`), how they draw (`presentation`), how they open (`open`);\n * - `dashboard`: sections about whole tables, which may embed a list view;\n * - `form`: one command's form, signed in or public (appForms.ts);\n * - `landing`: the public front page (appLanding.ts).\n *\n * Nav holds a view id, `{ to, label }` for a menu-only word, or a\n * `navGroup` — never an inline definition — and is optional: an App\n * without it has no menu. Every list, dashboard and signed-in form view\n * must be reachable (nav, home, a landing, or a reachable dashboard's embed or\n * note action); a view appears in nav at most once; public views are not in\n * nav, which is signed-in chrome.\n *\n * The App keeps the views it opens as rooms — every view nav references,\n * every list or dashboard a home rule names (the one room of an App without\n * nav), and every list or dashboard a reachable dashboard embeds or names in\n * a note action — under their keys, nav as the tree it was written as, and the home rules as written, for the server\n * to evaluate per visitor. A list view keeps its own words, with its action\n * selector; the New form, the actions and the captions stay the resource's.\n * A dashboard keeps its sections, an embed drawn as the list or board\n * section its view reads as and naming that view, whose room its \"open →\"\n * opens. Every form is kept under its key, public ones too; the landing\n * under its own.\n */\nimport { tabKey } from '@mistriwork/engine/shared/addressParams.js';\nimport { filterAddressWord, listFilterKey } from '@mistriwork/engine/shared/listFilters.js';\nimport { parseViewImport } from './appImport.js';\nimport { LABEL_SEGMENT } from '@mistriwork/engine/shared/recordLabel.js';\nimport { parseCaption } from '@mistriwork/engine/shared/appMessages.js';\nimport { andQuery, splitClauseTail } from '@mistriwork/engine/shared/query.js';\nimport { segmentBookOf, unfoldSegments } from './appSegments.js';\nimport { PREVIOUS_PERIOD } from './dashWindow.js';\nimport { WIDTHS, type SectionWidth } from '@mistriwork/engine/shared/page.js';\nimport { parseTableSizing } from '@mistriwork/engine/shared/tableSizing.js';\nimport { parseListEmpty } from '@mistriwork/engine/shared/listEmpty.js';\nimport { ASSISTANT_ROOM, parseRoutePath } from './appAddress.js';\nimport { destinationProblem, type DestinationKind } from './appDestinations.js';\nimport { parseAppLinkAction } from '@mistriwork/engine/shared/appLinks.js';\nimport { checkGreeting } from './appGreeting.js';\nimport {\n BETTER,\n DASH_CHART_SHAPES,\n DASH_KINDS,\n FILTER_BARS,\n OPEN_MODES,\n SECTION_KEYS,\n VIEW_SECTION_KEYS,\n navViewKeys,\n parseRealm,\n resourceCeiling,\n type AppAuth,\n type AppRoleGrant,\n type AppDashSection,\n type DashTarget,\n type FilterBar,\n type AppDashboardView,\n type AppFormView,\n type AppHomeRule,\n type AppLanding,\n type AppListView,\n type AppNavNode,\n DEFAULT_WINDOWS,\n} from './appModel.js';\nimport {\n FIGURE_FORMATS,\n FIGURE_FORMAT_WORDS,\n DISPLAY_TONES,\n VIEW_KEY,\n allowKeys,\n humanName,\n isRecord,\n keyHomesOf,\n optionalText,\n} from '@mistriwork/engine/shared/model.js';\nimport {\n CALENDAR_BACKDROPS_MAX,\n type AppBoardFact,\n type AppCalendarBackdrop,\n type AppListPresentation,\n type AppListTab,\n type AppSplitListDetail,\n} from '@mistriwork/engine/shared/listPresentations.js';\nimport { parseEditable } from './appListEditable.js';\nimport { parseFormView } from './appForms.js';\nimport { headingOf } from '@mistriwork/engine/shared/appNoun.js';\nimport { landingViews, parseLandingView } from './appLanding.js';\nimport { CARDS_MAX_LINES, parseCards } from '@mistriwork/engine/shared/appCards.js';\nimport { checkDeclaredResource, checkResourceSlot, type ParsedData } from './appResourceBlocks.js';\nimport { parseHome, type HomeView } from './appHome.js';\nimport { parseIcon } from '@mistriwork/engine/shared/appIcons.js';\nimport { unknownKey } from '@mistriwork/engine/shared/suggest.js';\nimport { isAreaName } from '@mistriwork/engine/shared/dashLayout.js';\nimport {\n embedInteractive,\n firstRunSection,\n nameSectionWindows,\n parseActivitySection,\n parseCustomDashSection,\n parseFindSection,\n parseSectionWindows,\n} from './appDashSections.js';\nimport { parseCustomList, parsePage } from '@mistriwork/engine/shared/appComponents.js';\nimport { conditionsOnly, parseChartShape, parseAreas, parseFunnelStages, parseNoteBanner, parseStepsShape } from './appDashShapes.js';\nimport { AppProblems, gather } from './appProblems.js';\nimport { autoKeyOf } from '@mistriwork/engine/shared/autoBucket.js';\nimport { parseOpenRoles } from './appViewRoles.js';\nimport { parseNav } from './appNav.js';\nimport { isRegisteredCommand, registeredCommand } from './command.js';\n\n/* Each view as parsed: the AppDef view itself, beside what only parsing\n needs — its key and where it was written, and a dashboard's tables and\n embeds, which the home and reachability checks read. */\ninterface ListView {\n kind: 'list';\n key: string;\n at: string;\n view: AppListView;\n}\ninterface DashboardView {\n kind: 'dashboard';\n key: string;\n at: string;\n view: AppDashboardView;\n /** Every table a section reads, embeds included: what a home rule's roles must read. */\n tables: string[];\n embeds: string[];\n}\ninterface LandingView {\n kind: 'landing';\n key: string;\n at: string;\n raw: Record<string, unknown>;\n}\ninterface FormView {\n kind: 'form';\n key: string;\n at: string;\n form: AppFormView;\n}\ntype ParsedView = ListView | DashboardView | LandingView | FormView;\n\n/** A parsed view's kind, as a destination reads it (appDestinations.ts). */\nconst parsedKind = (view: ParsedView | undefined): DestinationKind | undefined =>\n view === undefined ? undefined : view.kind === 'form' ? (view.form.public ? 'public form' : 'form') : view.kind;\n\ninterface ContentContext {\n data: ParsedData;\n name: string;\n auth?: AppAuth;\n api?: import('./appApi.js').AppApiDef;\n}\ninterface ParsedContent {\n views: Record<string, AppListView | AppDashboardView | AppFormView>;\n nav: AppNavNode[];\n home: AppHomeRule[];\n landing?: { view: string; content: AppLanding };\n}\n\n/** A view's Favorites star is opt-in: true, or false, which is the same as leaving it out. */\nfunction favoriteFlag(value: unknown, at: string): boolean {\n if (value === undefined) return false;\n if (typeof value !== 'boolean') throw new Error(`${at}: \"favorite\" must be true (a star on the view) or false.`);\n return value;\n}\n\n/* Spellings renamed before 1.0, refused with the word that replaced them (allowKeys' `refused`). */\nconst LIST_VIEW_RENAMED = {\n table: 'is now \"from\": the resource whose rows the list shows',\n keys: 'is now \"shortcuts\": a split\\'s single keys for the picked record\\'s actions',\n} as const;\nconst BACKDROP_RENAMED = {\n resource: 'is now \"from\", and the date columns a span runs between are \"start\" and \"end\"',\n to: 'is now \"end\"',\n label: 'is now \"labelColumn\": the column whose value names a span',\n} as const;\n/* Captioned values are `properties` (a fields section's presentation of that name); `facts` is the dotted line only. */\nconst PRESENTATION_RENAMED = {\n queue: {\n facts: 'is now \"properties\": the captioned values under the title (a dotted line is a card\\'s \"facts\")',\n },\n cards: {\n lines: 'is now \"properties\": the captioned values under the title (\"facts\" stays the dotted line)',\n },\n} as const;\nconst DASHBOARD_VIEW_RENAMED = {\n table: 'is now \"from\": the resource every section that names none reads',\n layout: 'is now \"areas\": the rows of area names its sections stand in (\"layout\" is a page\\'s archetype)',\n refresh: 'is gone: a dashboard follows changes as they happen and re-reads what depends on the time each minute',\n} as const;\nexport const LIST_VIEW_KEYS = [\n 'kind',\n 'from',\n 'label',\n 'path',\n 'icon',\n 'query',\n // Read before the view is (appSegments.ts unfoldSegments): the segment's word joins `query`.\n 'segment',\n 'show',\n 'presentation',\n 'board',\n 'split',\n 'empty',\n 'tabs',\n 'filters',\n 'filterBar',\n 'suggest',\n 'advancedFilters',\n 'quickAdd',\n 'favorite',\n 'totals',\n 'density',\n 'rowActions',\n 'editable',\n 'open',\n 'opens',\n 'sizing',\n 'actions',\n 'sort',\n 'order',\n 'import',\n 'shortcuts',\n 'heading',\n 'roles',\n 'count',\n] as const;\nexport const DASHBOARD_VIEW_KEYS = [\n 'kind',\n 'label',\n 'path',\n 'heading',\n 'description',\n 'icon',\n 'from',\n 'sections',\n 'aside',\n 'areas',\n 'windows',\n 'window',\n 'windowControl',\n 'filters',\n 'favorite',\n 'roles',\n] as const;\nexport const PRESENTATION_KEYS = {\n checklist: ['kind', 'primary', 'checked', 'lines'],\n queue: ['kind', 'primary', 'lines', 'properties', 'badge', 'openLabel'],\n board: ['kind', 'by', 'lanes', 'order', 'swimlanes', 'limits', 'primary', 'lines', 'category', 'footer', 'sum', 'folded'],\n calendar: ['kind', 'date', 'end', 'primary', 'lines', 'badge', 'backdrop'],\n tree: ['kind', 'parent', 'primary', 'lines'],\n gantt: ['kind', 'start', 'end', 'primary', 'by', 'progress'],\n cards: ['kind', 'primary', 'description', 'value', 'badge', 'icon', 'properties', 'facts', 'footer', 'image', 'media'],\n split: ['kind', 'detail', 'limit', 'row'],\n} as const;\n/** How many `lines` a card below its primary may carry, by presentation. */\nconst MAX_LINES = {\n checklist: 1,\n queue: 2,\n board: 1,\n calendar: 1,\n tree: 1,\n cards: CARDS_MAX_LINES,\n} as const;\n/* A sort as the query bar writes one: columns or label paths, each with an optional direction. */\nconst SORT_TEXT = /^\\s*[A-Za-z_][\\w.]*(\\s+(asc|desc))?(\\s*,\\s*[A-Za-z_][\\w.]*(\\s+(asc|desc))?)*\\s*$/i;\n\nexport function parseContent(a: Record<string, unknown>, where: string, ctx: ContentContext): ParsedContent {\n if (a.views !== undefined && !isRecord(a.views)) throw new Error(`${where}: \"views\" must be an object of views keyed by id.`);\n const entries = Object.entries((a.views ?? {}) as Record<string, unknown>);\n const views = new Map<string, ParsedView>();\n /* Two passes: a dashboard's embeds read the list views, so every other\n view is parsed first. */\n /* Each view is judged on its own, so one run names every view's refusal\n (appProblems.ts); the checks across views wait until all of them parse. */\n const problems: string[] = [];\n const failed = new Set<string>();\n const book = segmentBookOf(ctx.data.resources);\n const unfolded = new Map<string, Record<string, unknown>>();\n for (const [key, authored] of entries) {\n const at = `${where}: views \"${key}\"`;\n const parsed = gather(problems, () => {\n if (!VIEW_KEY.test(key)) throw new Error(`${at}: a view key is its address, and must be lowercase letters, digits and hyphens.`);\n if (!isRecord(authored)) throw new Error(`${at} is not an object.`);\n // A segment named anywhere in the view is its word in a query from here on (appSegments.ts).\n const value = unfoldSegments(authored, book, at) as Record<string, unknown>;\n unfolded.set(key, value);\n const kind = viewKind(value, at);\n if (kind === 'list')\n views.set(key, {\n kind,\n key,\n at,\n view: withOpenRoles(parseListView(value, at, ctx.data), value, at, ctx),\n });\n else if (kind === 'form')\n views.set(key, {\n kind,\n key,\n at,\n form: withOpenRoles(parseFormView(value, at), value, at, ctx),\n });\n else if (kind === 'landing') views.set(key, { kind, key, at, raw: value });\n return true;\n });\n if (!parsed) failed.add(key);\n }\n for (const [key, value] of entries) {\n if (failed.has(key)) continue;\n const said: string[] = [];\n gather(said, () => {\n const at = `${where}: views \"${key}\"`;\n const raw = unfolded.get(key) ?? (value as Record<string, unknown>);\n const parsed =\n raw.kind === 'dashboard'\n ? parseDashboardView(raw, key, at, ctx.data, views, Boolean(ctx.auth))\n : raw.kind === 'page'\n ? parsePageView(raw, key, at, ctx)\n : undefined;\n if (parsed)\n views.set(key, {\n ...parsed,\n view: withOpenRoles(parsed.view, raw, at, ctx),\n });\n });\n /* A dashboard over a list that did not parse would only restate that list's refusal. */\n problems.push(...said.filter((problem) => ![...failed].some((bad) => problem.includes(`\"${bad}\"`))));\n }\n if (problems.length) throw new AppProblems(problems);\n /* The checks across views are independent of each other too: each is gathered, so the menu's typo and the home's\n are said in one run, and only the reachability walk, which reads both, waits until they parse. */\n const across: string[] = [];\n const order = entries.map(([key]) => key);\n const apiViews = ctx.api?.views.map((entry) => entry.view) ?? [];\n for (const key of apiViews) {\n if (views.get(key)?.kind !== 'list') across.push(`${where}: api.views.${key} must reference an existing list view.`);\n }\n const ofKind = <K extends ParsedView['kind']>(kind: K) =>\n order.map((key) => views.get(key)!).filter((view): view is Extract<ParsedView, { kind: K }> => view.kind === kind);\n\n const forms = ofKind('form');\n if (forms.length && !ctx.auth?.roles) {\n across.push(`${where}: form views require \"auth\" and \"auth.roles\". A form runs a command, and commands need both.`);\n } else gather(across, () => checkForms(forms, ctx));\n for (const source of ofKind('list')) {\n const said = [\n { at: `${source.at}, \"empty.action.view\"`, action: source.view.empty?.action },\n ...(source.view.tabs ?? []).map((tab, i) => ({ at: `${source.at}, tab ${i + 1}, \"empty.action.view\"`, action: tab.empty?.action })),\n ];\n for (const { at, action } of said) {\n if (!action) continue;\n /* A room, by the one rule every action's destination answers to (appDestinations.ts): \"No canned responses:\n write one\" opens the signed-in form that writes one. */\n const problem = destinationProblem(at, action.view, parsedKind(views.get(action.view)));\n if (problem) across.push(problem);\n }\n }\n /* A dashboard's note, a banner too, opens a room by the same rule. Its target becomes a room only when this\n dashboard is reached from nav, home, a landing or a reachable dashboard; final payload projection still\n removes lost targets. */\n for (const source of ofKind('dashboard')) {\n const placed = [\n ...source.view.sections.map((section, i) => ({ section, at: `${source.at}, section ${i + 1}` })),\n ...(source.view.aside ?? []).map((section, i) => ({ section, at: `${source.at}, \"aside\", section ${i + 1}` })),\n ];\n for (const { section, at } of placed) {\n if (section.kind !== 'note' || !section.action || 'to' in section.action) continue;\n const problem = destinationProblem(`${at}, note action`, section.action.view, parsedKind(views.get(section.action.view)));\n if (problem) across.push(problem);\n }\n }\n const publicForms = new Set(forms.filter((view) => view.form.public).map((view) => view.key));\n /* What a landing may open and show: the public forms, and — with\n `auth.public` — every list and dashboard whose tables the public reads\n all of, as a signed-out home may name (appHome.ts). */\n const publicReads = new Set(ctx.auth?.public?.read ?? []);\n const openLists = new Map(\n ofKind('list')\n .filter((view) => publicReads.has(view.view.table))\n .map((view) => [view.key, view.view]),\n );\n const openViews = new Set([\n ...openLists.keys(),\n ...ofKind('dashboard')\n .filter((view) => view.tables.length && view.tables.every((table) => publicReads.has(table)))\n .map((view) => view.key),\n ]);\n const images = new Set(\n Object.entries(ctx.data.resources ?? {})\n .filter(([name, resource]) => resource.file && publicReads.has(name))\n .map(([name]) => name),\n );\n const landingsSaid = across.length;\n const landings = ofKind('landing').flatMap((view) => {\n const landing = gather(across, () =>\n parseLandingView(view.raw, view.at, {\n forms: publicForms,\n views: openViews,\n lists: openLists,\n images,\n }),\n );\n return landing ? [{ key: view.key, landing }] : [];\n });\n /* A landing that did not parse names nothing: what it would have opened is not known, so nothing is judged unreached. */\n const landingsRead = across.length === landingsSaid;\n const landingKeys = landings.flatMap(({ landing }) => landingViews(landing));\n /* A landing is the signed-out front page: what it shows or opens is for visitors who hold no role. */\n for (const key of landingKeys) {\n const view = views.get(key);\n const roles =\n view?.kind === 'form' ? view.form.roles : view?.kind === 'list' || view?.kind === 'dashboard' ? view.view.roles : undefined;\n if (roles)\n across.push(\n `${view!.at} names \"roles\", and the landing opens it to signed-out visitors, who hold none. Remove \"roles\", or the landing's link to it.`,\n );\n }\n if (landings.length > 1)\n across.push(`${where}: views \"${landings[0].key}\" and \"${landings[1].key}\" are both landings. An App has one front page.`);\n if (landings.length && !ctx.auth) {\n across.push(\n `${where}: a landing view requires \"auth\". An App without sign-in has no signed-out front door — every visitor already sees the App.`,\n );\n }\n\n /* An assistant with a page of its own is a room nav may place, by its word, as a view is (appAssistant.ts). */\n const assistantPage =\n isRecord(a.assistant) && Array.isArray(a.assistant.placement) && (a.assistant.placement as unknown[]).includes('page');\n if (assistantPage && views.has(ASSISTANT_ROOM))\n across.push(\n `${where}: views \"${ASSISTANT_ROOM}\" takes the word the assistant's page is placed by (assistant placement 'page'). Give the view another key.`,\n );\n const nav = gather(across, () => parseNav(a.nav, where, views, order, parsedKind, assistantPage));\n const homeViews = new Map<string, HomeView>(\n order.map((key) => {\n const view = views.get(key)!;\n if (view.kind === 'form') {\n return [\n key,\n {\n kind: 'form',\n tables: [],\n command: view.form.command.id,\n ...(view.form.public ? { public: true as const } : {}),\n ...(view.form.roles ? { roles: view.form.roles } : {}),\n },\n ];\n }\n const roles = view.kind === 'landing' ? undefined : view.view.roles;\n return [\n key,\n {\n kind: view.kind,\n tables: view.kind === 'list' ? [view.view.table] : view.kind === 'dashboard' ? view.tables : [],\n ...(roles ? { roles } : {}),\n },\n ];\n }),\n );\n const rules = gather(across, () => parseHome(a.home, where, homeViews, ctx.auth, resourceCeiling(ctx.data).read, ctx.name));\n /* What nothing opens is judged only once the menu, the home and the landing are all known: one that did not parse\n would make every room it names look orphaned. */\n if (!nav || !rules || !landingsRead) throw new AppProblems(across);\n // The assistant's room is nav's to place and no view's: nothing below serves it as one.\n const navKeys = navViewKeys(nav).filter((key) => views.has(key));\n\n /* A list, dashboard or signed-in form view nothing can open is a typo or\n an orphan; either way it would go unserved without a word. A public\n form is opened at its own address. Dashboard embeds, the lists metrics\n open and note actions are\n traversed only after a root reaches their dashboard: two orphan notes\n pointing at each other never manufacture a room. */\n /* A list a record's page embeds is opened by that page: a thread's posts, drawn only on the thread. */\n const pageEmbeds = Object.values(isRecord(a.pages) ? a.pages : {}).flatMap((page) =>\n (isRecord(page) && Array.isArray(page.sections) ? page.sections : []).flatMap((section) =>\n isRecord(section) && section.kind === 'view' && typeof section.view === 'string' ? [section.view] : [],\n ),\n );\n /* The header's New menu opens its views and the forms of its commands (ui.shell.new; app.ts checks the names). */\n const rawNew = (isRecord(a.ui) && isRecord(a.ui.shell) && Array.isArray(a.ui.shell.new) ? a.ui.shell.new.filter(isRecord) : []).flatMap(\n (entry) => (Array.isArray(entry.children) ? entry.children.filter(isRecord) : [entry]),\n );\n const newKeys = [\n ...rawNew.flatMap((entry) => (typeof entry.view === 'string' && views.has(entry.view) ? [entry.view] : [])),\n ...forms.filter(({ form }) => !form.public && rawNew.some((entry) => entry.command === form.command.id)).map(({ key }) => key),\n ];\n const reachable = new Set([...navKeys, ...rules.map((rule) => rule.view), ...landingKeys, ...apiViews, ...pageEmbeds, ...newKeys]);\n for (const key of reachable) {\n const view = views.get(key);\n if (view?.kind !== 'dashboard') continue;\n for (const target of [\n ...view.embeds,\n ...[...view.view.sections, ...(view.view.aside ?? [])].flatMap((section) =>\n section.kind === 'note' && section.action && 'view' in section.action\n ? [section.action.view]\n : section.kind === 'metric' && section.view\n ? [section.view]\n : section.kind === 'firstRun'\n ? (section.steps ?? []).map((step) => step.view)\n : [],\n ),\n ])\n reachable.add(target);\n }\n for (const key of order) {\n const view = views.get(key)!;\n if (view.kind === 'form' && !view.form.public && !reachable.has(key)) {\n across.push(\n `${view.at} is a signed-in form referenced by nothing — not nav, not home, not the New menu (ui.shell.new) — so nothing could open it. Reference it, or remove it.`,\n );\n }\n if (view.kind === 'dashboard' && !reachable.has(key)) {\n across.push(\n `${view.at} is referenced by nothing — not nav, not home, not a reachable dashboard's embed, metric or note action, or a landing's { kind: 'view' } section — so nothing could open it. Reference it, or remove it.`,\n );\n }\n }\n if (across.length) throw new AppProblems(across);\n /* A list nothing links to is still a list: a view is everything a visitor\n can open, and `nav` arranges the menu over some of them. An unlinked one\n is served at its address and offered by ⌘K, and no menu lists it — the\n lookup lists an ops App keeps (a code table, a log) without crowding the\n side menu. A dashboard stays refused: what it opens is only reached\n through it, and an orphan pair must not manufacture rooms. */\n for (const key of order) if (views.get(key)!.kind === 'list') reachable.add(key);\n /* So is a free-form page: served at its address, which another page or a component may link to with nav: false. */\n for (const key of order) {\n const view = views.get(key)!;\n if (view.kind === 'dashboard' && view.view.page) reachable.add(key);\n }\n\n /* The served set includes every ordinary root and every room a reachable\n dashboard opens. Navigation still decides the sidebar; an action target\n is a room only after its source dashboard is independently reachable. */\n const served: Record<string, AppListView | AppDashboardView | AppFormView> = {};\n for (const key of reachable) {\n const view = views.get(key)!;\n if (view.kind === 'landing' || (view.kind === 'form' && view.form.public)) continue;\n served[key] = view.kind === 'form' ? view.form : view.view;\n }\n /* Then the public forms, which no menu names: each is served at its own address. */\n for (const view of forms) if (view.form.public) served[view.key] = view.form;\n const [landing] = landings;\n return {\n views: served,\n nav,\n home: rules.map(({ at: _at, ...rule }) => rule),\n ...(landing ? { landing: { view: landing.key, content: landing.landing } } : {}),\n };\n}\n\n/*\n * A form's command is granted like any other, and for a signed-in form the\n * grant is its whole audience: one whose command no role holds could be\n * opened by nobody. A command a form runs creates its rows and names none,\n * so no action runs it on a record too.\n */\nfunction checkForms(forms: readonly FormView[], ctx: ContentContext): void {\n const grants = Object.values(ctx.auth?.roles ?? {});\n const actions = Object.entries(ctx.data.resources ?? {}).flatMap(([name, resource]) =>\n (resource.actions ?? []).flatMap((action) => (action.command ? [{ name, id: action.command.id }] : [])),\n );\n for (const { at, form } of forms) {\n const id = form.command.id;\n const acting = actions.find((action) => action.id === id);\n if (acting) {\n throw new Error(\n `${at} runs the command \"${id}\", which an action on resources \"${acting.name}\" also runs. ` +\n \"A form's command creates records and names none; an action's names the record it acts on. Register a command for each.\",\n );\n }\n if (form.public) continue;\n /* Under \"auth.rows\" a signed-in form is served too: its command runs in\n the member's scope, so a row it inserts must land inside it (the scoped\n transaction refuses one that does not), and a tenant's owner column is\n the runtime's to fill, never the handler's. */\n if (!grants.some((grant) => grant.commands?.includes(id))) {\n throw new Error(\n `${at} runs the command \"${id}\", which no role in auth.roles is granted, so nobody could open the form. Grant it in auth.roles.<role>.commands.`,\n );\n }\n }\n}\n\nfunction viewKind(value: Record<string, unknown>, at: string): ParsedView['kind'] {\n const kind = value.kind;\n if (kind === undefined) {\n /* A view without its kind: say which one its keys suggest. */\n if (Object.hasOwn(value, 'sections')) throw new Error(`${at} has no \"kind\"; a dashboard view says kind: 'dashboard'.`);\n if (Object.hasOwn(value, 'from')) throw new Error(`${at} has no \"kind\"; a list view says kind: 'list'.`);\n throw new Error(\n `${at} has no \"kind\". A view is one of: list, dashboard, form or landing, written with listView(), dashboard(), commandForm() or landing(); a free-form page is a pages/*.page.tsx file.`,\n );\n }\n if (kind !== 'list' && kind !== 'dashboard' && kind !== 'landing' && kind !== 'form' && kind !== 'page') {\n throw new Error(\n `${at} has a \"kind\" of \"${String(kind)}\". A view is one of: list, dashboard, form or landing, written with listView(), dashboard(), commandForm() or landing(); a free-form page is a pages/*.page.tsx file.`,\n );\n }\n /* A free-form page is served as the dashboard that draws it (parsePageView). */\n return kind === 'page' ? 'dashboard' : kind;\n}\n\n/* ---------- list views ---------- */\n\n/* A board or a split is a key of the list view that carries its options, so the view says which it is by the key and\n the two cannot be written together; the rest draw through `presentation`, written with their helpers. Both parse to\n the one `presentation` the rest of the platform reads. */\nfunction listPresentation(v: Record<string, unknown>, at: string): AppListPresentation | undefined {\n const keyed = (['board', 'split'] as const).filter((key) => v[key] !== undefined);\n const kind = isRecord(v.presentation) ? v.presentation.kind : undefined;\n if (kind === 'board' || kind === 'split') {\n throw new Error(\n `${at} has a \"presentation\" of kind \"${kind}\". A ${kind} is a key of the list view: ${kind}: { ${kind === 'board' ? \"by: 'status', primary: 'name'\" : \"detail: ['name']\"} }.`,\n );\n }\n if (keyed.length + (v.presentation === undefined ? 0 : 1) > 1) {\n throw new Error(\n `${at} has ${[...keyed.map((key) => `\"${key}\"`), ...(v.presentation === undefined ? [] : ['\"presentation\"'])].join(' and ')}. A list draws its rows one way: a board, a split or a presentation.`,\n );\n }\n if (keyed.length === 0) return v.presentation === undefined ? undefined : parsePresentation(v.presentation, at);\n const key = keyed[0];\n if (!isRecord(v[key])) throw new Error(`${at}: \"${key}\" must be an object${key === 'board' ? ' with at least \"by\"' : ''}.`);\n allowKeys(\n v[key] as Record<string, unknown>,\n PRESENTATION_KEYS[key].filter((name) => name !== 'kind'),\n `${at}, \"${key}\"`,\n );\n return parsePresentation({ ...(v[key] as object), kind: key }, at);\n}\n\n/* A text filter's suggestions: the distinct values of a few of the list's own columns, offered as the reader types.\n Keyed by the filter they fill, which the list must declare: a suggestion for a box nobody draws is a typo. */\nfunction parseSuggest(raw: unknown, filters: string[] | undefined, at: string): Record<string, string[]> {\n const where = `${at}, \"suggest\"`;\n if (!isRecord(raw) || !Object.keys(raw).length)\n throw new Error(\n `${where} must name a text filter and the columns whose values it suggests, like suggest: { title: ['make', 'model'] }.`,\n );\n const declared = (filters ?? []).map((name) => name.trim());\n return Object.fromEntries(\n Object.entries(raw).map(([filter, columns]) => {\n if (!declared.includes(filter))\n throw new Error(`${where} names \"${filter}\", which is not one of this list's \"filters\". Suggestions fill a filter the list draws.`);\n if (\n !Array.isArray(columns) ||\n !columns.length ||\n columns.some((column) => typeof column !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(column.trim()))\n )\n throw new Error(`${where}, \"${filter}\" must be a list of the resource's own column names, like ['make', 'model'].`);\n return [filter, [...new Set((columns as string[]).map((column) => column.trim()))]];\n }),\n );\n}\n\nfunction parseListView(v: Record<string, unknown>, at: string, data: ParsedData): AppListView {\n allowKeys(v, [...LIST_VIEW_KEYS, 'realm'], at, LIST_VIEW_RENAMED);\n if (typeof v.from !== 'string' || !v.from.trim()) throw new Error(`${at} has no \"from\": a list view shows the rows of one resource.`);\n const table = v.from.trim();\n checkResourceSlot(table, `${at}, \"from\"`, data);\n checkDeclaredResource(table, `${at}, \"from\"`, data);\n const resource = data.resources![table];\n if (v.advancedFilters !== undefined && typeof v.advancedFilters !== 'boolean') {\n throw new Error(`${at}: \"advancedFilters\" must be true or false.`);\n }\n if (typeof v.filterBar === 'boolean')\n throw new Error(`${at}: \"filterBar\" is now one of 'bar', 'rail' or 'none': ${v.filterBar ? \"true is 'bar'\" : \"false is 'none'\"}.`);\n if (v.filterBar !== undefined && !FILTER_BARS.includes(v.filterBar as FilterBar)) {\n throw new Error(`${at}: \"filterBar\" must be 'bar', 'rail' (the filters as a rail of facets beside the rows) or 'none'.`);\n }\n if (v.quickAdd !== undefined && typeof v.quickAdd !== 'boolean') {\n throw new Error(`${at}: \"quickAdd\" must be true (inline resources.${table}.form, or the schema's New form) or false.`);\n }\n if (v.open === 'peek') throw new Error(`${at} has an \"open\" of \"peek\", which is now \"drawer\", as the New form's is.`);\n if (v.open !== undefined && !(OPEN_MODES as readonly unknown[]).includes(v.open)) {\n throw new Error(`${at} has an \"open\" of \"${String(v.open)}\". Use \"page\", \"drawer\" or \"modal\".`);\n }\n if (v.filters !== undefined && !Array.isArray(v.filters)) throw new Error(`${at} has \"filters\" that are not a list of column names.`);\n /* A filter across a reference or over the rows that point at the record is held as the words its condition\n starts with (listFilters.ts), so everything downstream reads one kind of filter. */\n const filters = (v.filters as unknown[] | undefined)?.map((filter) => listFilterKey(filter, at));\n const words = new Map<string, string>();\n for (const filter of filters ?? []) {\n const word = filterAddressWord(filter);\n if (words.has(word)) throw new Error(`${at} names the filter \"${words.get(word)}\" twice.`);\n words.set(word, filter);\n }\n const suggest = v.suggest === undefined ? undefined : parseSuggest(v.suggest, filters, at);\n const importing = v.import === undefined ? undefined : parseViewImport(v.import, at);\n if (importing && !importing.command && resource.access !== 'write') {\n throw new Error(\n `${at} has \"import\" over resources \"${table}\", which the App does not write; importing creates records, which needs access 'write'.`,\n );\n }\n const presentation = listPresentation(v, at);\n if (presentation?.kind === 'calendar')\n for (const [i, span] of (presentation.backdrop ?? []).entries()) {\n const here = `${at}, presentation, backdrop${(presentation.backdrop?.length ?? 0) > 1 ? ` ${i + 1}` : ''}, \"from\"`;\n checkResourceSlot(span.from, here, data);\n checkDeclaredResource(span.from, here, data);\n }\n const empty = v.empty === undefined ? undefined : parseListEmpty(v.empty, at);\n if (empty && (presentation?.kind === 'calendar' || presentation?.kind === 'tree' || presentation?.kind === 'gantt')) {\n throw new Error(\n `${at} has \"empty\" and a ${presentation.kind} presentation. A ${presentation.kind} reads its own rows, so use its ordinary empty state.`,\n );\n }\n /* A split's list is the narrow pane beside a record; a rail beside it would leave the rows no room. */\n if (v.filterBar === 'rail' && presentation?.kind === 'split') {\n throw new Error(\n `${at} has filterBar 'rail' and a split presentation. A rail sits beside a full list; a split's list keeps the bar (or filterBar: 'none').`,\n );\n }\n /* A board over a machine's column reads its lanes from the machine (app.ts withMachineLanes): one order, said once. */\n if (presentation?.kind === 'board' && presentation.lanes !== undefined && resource?.machine?.column === presentation.by) {\n throw new Error(\n `${at}: presentation \"lanes\" orders \"${presentation.by}\", which the machine of resources.${table} moves; ` +\n 'its lanes are the machine\\'s states in its order. Drop \"lanes\".',\n );\n }\n /* A card's place is the reader's to change by hand; a column the resource moves only through its actions never is. */\n if (presentation?.kind === 'board' && presentation.order !== undefined && resource?.machine?.column === presentation.order) {\n throw new Error(\n `${at}: presentation \"order\" names \"${presentation.order}\", which the machine of resources.${table} moves. ` +\n \"A card's place is the reader's to change; name another numeric column.\",\n );\n }\n let totals: string[] | undefined;\n if (v.totals !== undefined) {\n if (\n !Array.isArray(v.totals) ||\n !v.totals.length ||\n v.totals.some((c) => typeof c !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(c))\n ) {\n throw new Error(`${at} has \"totals\" that are not a list of the resource's own numeric columns.`);\n }\n if (new Set(v.totals).size !== v.totals.length) throw new Error(`${at} has \"totals\" that name one column twice.`);\n if (presentation) throw new Error(`${at} has \"totals\" and a ${presentation.kind} presentation; a footer of totals belongs to a grid.`);\n totals = v.totals as string[];\n }\n if (v.rowActions !== undefined) {\n if (typeof v.rowActions !== 'boolean') throw new Error(`${at}: \"rowActions\" must be true (a ⋯ menu on each row) or false.`);\n if (v.rowActions && presentation)\n throw new Error(`${at} has \"rowActions\" and a ${presentation.kind} presentation; a row menu belongs to a grid.`);\n }\n const editable = parseEditable(v, at, table, resource, presentation);\n if (v.density !== undefined && v.density !== 'comfortable' && v.density !== 'compact') {\n throw new Error(`${at} has a \"density\" of \"${String(v.density)}\". Use \"comfortable\" or \"compact\".`);\n }\n /* Row height and column sizing are a grid's. A split's list is drawn as\n one; the card presentations draw no grid for either to reach. */\n for (const key of ['density', 'sizing'] as const) {\n if (v[key] !== undefined && presentation && presentation.kind !== 'split') {\n throw new Error(\n `${at} has \"${key}\" and a ${presentation.kind} presentation; ${key === 'density' ? 'a row height' : 'column sizing'} belongs to a grid.`,\n );\n }\n }\n /* A join table's list opens the record each row is about. The grid's rows only: a card, a lane or a\n split's pick is drawn and acted on as the row it is. */\n if (v.opens !== undefined) {\n if (typeof v.opens !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(v.opens.trim())) {\n throw new Error(`${at}: \"opens\" must name one of the resource's reference columns, whose record a row opens.`);\n }\n if (presentation)\n throw new Error(`${at} has \"opens\" and a ${presentation.kind} presentation; a row opens another record from a grid only.`);\n }\n if (presentation?.kind === 'split' && v.open !== undefined) {\n throw new Error(`${at} has \"open\" and a split presentation, which opens its rows in its own detail pane. Remove \"open\".`);\n }\n let actions: string[] | undefined;\n if (v.actions !== undefined) {\n if (!Array.isArray(v.actions)) throw new Error(`${at} has \"actions\" that are not a list of action ids.`);\n if (presentation?.kind !== 'queue' && presentation?.kind !== 'board' && presentation?.kind !== 'custom') {\n throw new Error(\n `${at} has \"actions\", which only a queue, a board or a custom list takes: a grid, a checklist or a split draws no actions, and a record page offers every one of resources.${table}.actions.`,\n );\n }\n const declared = (resource.actions ?? []).map((action) => action.id);\n actions = v.actions.map((id, i) => {\n if (typeof id !== 'string' || !id.trim()) {\n throw new Error(`${at}, \"actions\" entry ${i + 1} is not an action id: a view lists the ids of resources.${table}.actions.`);\n }\n if (!declared.includes(id.trim())) {\n throw new Error(\n `${at}, \"actions\" names \"${id.trim()}\", which resources.${table}.actions does not declare. ` +\n (declared.length ? `It declares: ${declared.join(', ')}.` : 'It declares no actions.'),\n );\n }\n return id.trim();\n });\n if (new Set(actions).size !== actions.length) throw new Error(`${at} has \"actions\" that name one action twice.`);\n }\n /* The list's order, one key: \"created_on desc\" is how it reads when\n nothing else says one, and a list of named orders is the Sort menu a\n reader chooses from, the first of them the order the list opens in.\n Written like the query bar, so a chosen one is the query's own sort\n clause and the address carries it; the opening one is kept out of the\n bar, as a hand order is: it is the view's reading, not a question\n anyone asked. A tab's sort clause wins over it, and the menu over both.\n A hand order already is one, and a sort in the standing query would\n read every tab its own way. */\n let sort: string | undefined;\n let sorts: Array<{ label: string; sort: string }> | undefined;\n if (v.sort !== undefined) {\n if (Array.isArray(v.sort)) {\n if (v.sort.length < 2 || v.sort.length > 8)\n throw new Error(`${at} has a \"sort\" list that is not two to eight { label, sort }: one order is a string, \"created_on desc\".`);\n sorts = v.sort.map((entry, i) => {\n const here = `${at}, sort ${i + 1}`;\n if (!isRecord(entry)) throw new Error(`${here} is not an object { label, sort }.`);\n allowKeys(entry, ['label', 'sort'], here);\n if (typeof entry.label !== 'string' || !entry.label.trim() || entry.label.trim().length > 40)\n throw new Error(`${here} needs a \"label\" of at most 40 characters.`);\n if (typeof entry.sort !== 'string' || !SORT_TEXT.test(entry.sort)) {\n throw new Error(`${here} has a \"sort\" that is not written like the query bar: \"price\", \"price desc\", \"year desc, mileage\".`);\n }\n return {\n label: entry.label.trim(),\n sort: entry.sort.trim().replace(/\\s+/g, ' '),\n };\n });\n if (new Set(sorts.map((entry) => entry.label)).size !== sorts.length)\n throw new Error(`${at} has a \"sort\" list with one label twice.`);\n sort = sorts[0]!.sort;\n } else if (typeof v.sort !== 'string' || !SORT_TEXT.test(v.sort)) {\n throw new Error(\n `${at} has a \"sort\" that is not written like the query bar (\"created_on desc\", \"year desc, mileage\") or a list of named orders, [{ label: 'Newest', sort: 'listed_on desc' }, …].`,\n );\n } else sort = v.sort.trim().replace(/\\s+/g, ' ');\n if (v.order !== undefined) {\n throw new Error(\n `${at} has \"sort\" and \"order\": a hand order is the list's own reading already, kept in \"${String(v.order).trim()}\". Keep one.`,\n );\n }\n if (presentation?.kind === 'board' || presentation?.kind === 'calendar') {\n throw new Error(`${at} has \"sort\" and a ${presentation.kind} presentation, which orders its cards itself.`);\n }\n }\n /* A hand order: the numeric column each row's place is kept in, read\n ascending wherever nothing else sorts the question. A sort in the\n standing query would win on every tab, so the order could never be read\n and is refused; a tab's own sort, a header and the Sort menu are readings\n of their own, and hide the handles while they last. */\n let order: string | undefined;\n if (v.order !== undefined) {\n if (typeof v.order !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(v.order.trim())) {\n throw new Error(`${at}: \"order\" must name one of the resource's own numeric columns, holding each row's place in the list.`);\n }\n order = v.order.trim();\n if (presentation?.kind === 'board') {\n throw new Error(`${at} has \"order\" and a board presentation; a board keeps its cards' places in its presentation's \"order\".`);\n }\n if (\n presentation?.kind === 'calendar' ||\n presentation?.kind === 'split' ||\n presentation?.kind === 'tree' ||\n presentation?.kind === 'gantt'\n ) {\n const which = {\n calendar: 'puts each record on its day',\n split: 'reads its list beside a record',\n tree: 'puts each record under its parent',\n gantt: 'puts each record on its dates',\n }[presentation.kind];\n throw new Error(\n `${at} has \"order\" and a ${presentation.kind} presentation, which ${which}. ` +\n 'A hand order is for a grid, a checklist, a queue or cards.',\n );\n }\n if (resource?.machine?.column === order) {\n throw new Error(\n `${at}: \"order\" names \"${order}\", which the machine of resources.${table} moves. ` +\n \"A row's place is the reader's to change; name another numeric column.\",\n );\n }\n }\n /* Single keys for the picked record's actions, in a split: the pane's header shows each beside its verb.\n Authored `shortcuts`; kept as the view's `keys`. */\n let keys: Record<string, string> | undefined;\n if (v.shortcuts !== undefined) {\n if (presentation?.kind !== 'split')\n throw new Error(`${at} has \"shortcuts\", which a split takes: they act on the record picked beside the list.`);\n if (!isRecord(v.shortcuts) || !Object.keys(v.shortcuts).length)\n throw new Error(`${at}: \"shortcuts\" must be an object of one key to an action id: { e: 'escalate' }.`);\n const declared = (resource?.actions ?? []).map((action) => action.id);\n keys = {};\n for (const [key, id] of Object.entries(v.shortcuts)) {\n if (!/^[a-z0-9]$/.test(key) || key === 'j' || key === 'k') {\n throw new Error(`${at}, \"shortcuts\" has \"${key}\": a key is one lowercase letter or digit, and J and K are the walk's own.`);\n }\n if (typeof id !== 'string' || !declared.includes(id.trim())) {\n throw new Error(\n `${at}, \"shortcuts\" maps \"${key}\" to ${JSON.stringify(id)}, which resources.${table}.actions does not declare. ` +\n (declared.length ? `It declares: ${declared.join(', ')}.` : 'It declares no actions.'),\n );\n }\n keys[key] = id.trim();\n }\n if (new Set(Object.values(keys)).size !== Object.keys(keys).length)\n throw new Error(`${at} has \"shortcuts\" that give one action two keys.`);\n }\n if (v.heading !== undefined && v.heading !== false) throw new Error(`${at}: \"heading\" is false, to draw the list without its title.`);\n /* A standing query is the conditions every tab reads under: the order is `sort`, the columns `show`. */\n const standingQuery = optionalText(v, 'query', at);\n if (standingQuery !== undefined) conditionsOnly(standingQuery, at);\n return {\n table,\n ...(sorts ? { sorts } : {}),\n ...(sort ? { sort } : {}),\n ...(order ? { order } : {}),\n ...(v.label === undefined ? {} : { label: parseCaption(v.label, `${at}.label`) }),\n ...(v.path === undefined ? {} : { path: parseRoutePath(v.path, at) }),\n ...(v.icon === undefined ? {} : { icon: parseIcon(v.icon, at) }),\n ...(v.realm === undefined ? {} : { realm: parseRealm(v.realm, at) }),\n ...(standingQuery === undefined ? {} : { query: standingQuery }),\n ...(v.show === undefined ? {} : { show: parseShow(v.show, at) }),\n ...(presentation ? { presentation } : {}),\n ...(empty ? { empty } : {}),\n ...(v.tabs === undefined ? {} : { tabs: parseTabs(v.tabs, at) }),\n ...(filters === undefined ? {} : { filters }),\n ...(v.filterBar === undefined ? {} : { filterBar: v.filterBar as FilterBar }),\n ...(suggest ? { suggest } : {}),\n ...(v.advancedFilters === undefined ? {} : { advancedFilters: v.advancedFilters as boolean }),\n ...(v.quickAdd === true ? { quickAdd: true as const } : {}),\n ...(favoriteFlag(v.favorite, at) ? { favorite: true as const } : {}),\n ...(totals ? { totals } : {}),\n ...(v.density === undefined ? {} : { density: v.density as 'comfortable' | 'compact' }),\n ...(v.rowActions === true ? { rowActions: true as const } : {}),\n ...(editable ? { editable } : {}),\n ...(v.open === undefined ? {} : { open: v.open as AppListView['open'] }),\n ...(v.opens === undefined ? {} : { opens: (v.opens as string).trim() }),\n ...(v.sizing === undefined ? {} : { sizing: parseTableSizing(v.sizing, at) }),\n ...(actions ? { actions } : {}),\n ...(importing === undefined ? {} : { import: importing }),\n ...(keys ? { keys } : {}),\n ...(v.heading === false ? { heading: false as const } : {}),\n ...(countFlag(v.count, at) === undefined ? {} : { count: countFlag(v.count, at) }),\n };\n}\n\n/* A list's say on its menu count (shared/menuCounts.ts): true or false over the rule. */\nfunction countFlag(value: unknown, at: string): boolean | undefined {\n if (value === undefined) return undefined;\n if (typeof value !== 'boolean') throw new Error(`${at}: \"count\" is true or false: whether the menu counts this list.`);\n return value;\n}\n\n/**\n * A view's `show`: plain strings, each a column or a dotted label path. The\n * caption is the resource's (`resources.<table>.labels`), wherever it shows.\n */\nfunction parseShow(raw: unknown, at: string): string[] {\n if (!Array.isArray(raw)) throw new Error(`${at} has a \"show\" that is not a list of column names.`);\n return raw.map((entry, i) => {\n if (typeof entry !== 'string' || !entry.trim()) throw new Error(`${at}, \"show\" entry ${i + 1} is not a column name or a label path.`);\n return entry.trim();\n });\n}\n\n/** What a list that names nothing is called: its records, several of them, as the resource's noun says, else its table's name. */\nfunction listLabel(table: string, data: ParsedData): string {\n const noun = data.resources?.[table]?.noun;\n return noun ? headingOf(noun.many) : humanName(table);\n}\n\n/** A dashboard section's captions: the resource's word for each shown column it has one for. */\nfunction labelsFor(show: readonly string[] | undefined, labels: Record<string, string> | undefined): Record<string, string> | undefined {\n const named = (show ?? []).filter((column) => labels !== undefined && Object.hasOwn(labels, column));\n return named.length ? Object.fromEntries(named.map((column) => [column, labels![column]])) : undefined;\n}\n\nfunction parseTabs(raw: unknown, at: string): AppListTab[] {\n if (!Array.isArray(raw)) throw new Error(`${at} has \"tabs\" that are not an array.`);\n return raw.map((entry, i) => {\n const here = `${at}, tab ${i + 1}`;\n if (!isRecord(entry)) throw new Error(`${here} is not an object.`);\n /* A tab's own order is its `sort`, kept as a sort clause after its conditions —\n \"status = open sort due_on\" — which every reader of the tab (its\n count, the address, a dashboard's embed) takes as it is, and which\n wins over the view's `sort`. */\n // `segment` is what a tab naming one unfolds to (appSegments.ts); its name is the tab's address.\n allowKeys(entry, ['label', 'query', 'segment', 'sort', 'empty'], here);\n if (typeof entry.label !== 'string' || !entry.label.trim()) throw new Error(`${here} has no \"label\".`);\n const query = optionalText(entry, 'query', here);\n const segment = typeof entry.segment === 'string' ? entry.segment : undefined;\n if (query !== undefined) conditionsOnly(query, here);\n if (entry.sort !== undefined && (typeof entry.sort !== 'string' || !SORT_TEXT.test(entry.sort))) {\n throw new Error(`${here} has a \"sort\" that is not written like the query bar: \"due_on\", \"created_on desc\", \"year desc, mileage\".`);\n }\n const sort = typeof entry.sort === 'string' ? entry.sort.trim().replace(/\\s+/g, ' ') : undefined;\n /* What the tab says while nothing is under it, as a list's `empty` says it for the list as it opens. */\n const empty = entry.empty === undefined ? undefined : parseListEmpty(entry.empty, here);\n return {\n label: entry.label.trim(),\n query: sort === undefined ? query : `${query ? `${query} ` : ''}sort ${sort}`,\n ...(segment ? { segment, slug: segment } : {}),\n ...(empty ? { empty } : {}),\n };\n });\n}\n\n/** A local column for a card: bare, non-empty, never a path. */\nfunction localColumn(o: Record<string, unknown>, key: string, at: string): string {\n const value = o[key];\n if (typeof value !== 'string' || !value.trim() || value.includes('.'))\n throw new Error(`${at}: \"${key}\" must name a local column, not a path.`);\n return value.trim();\n}\n\n/** A card's title: its own column, or one read through a reference, one hop (`vehicle_id.title`). */\nfunction cardTitle(value: unknown, at: string): string {\n if (typeof value !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*(\\.[A-Za-z_][A-Za-z0-9_]*)?$/.test(value.trim()))\n throw new Error(`${at}: \"primary\" must be a local column or a one-hop path like \"vehicle_id.title\".`);\n return value.trim();\n}\n\n/** A queue may say a referenced record's name, but never walk past one join. */\nfunction queueColumn(value: unknown, key: string, at: string): string {\n if (typeof value !== 'string' || !value.trim()) {\n throw new Error(`${at}: \"${key}\" must be a local column or a one-hop label path like \"employee_id.name\".`);\n }\n const name = value.trim();\n /* Keep legacy local names verbatim (apart from outer whitespace): before\n paths, a local column was any non-empty spelling without a dot. */\n if (!name.includes('.')) return name;\n if (/^[A-Za-z_][A-Za-z0-9_]*\\.[A-Za-z_][A-Za-z0-9_]*$/.test(name)) return name;\n throw new Error(`${at}: \"${key}\" must be a local column or a one-hop label path like \"employee_id.name\".`);\n}\n\n/**\n * A calendar's shaded spans: one kind or a list of them, each another\n * resource's rows from one of its date columns to another, labeled by a third\n * and shaded in a display tone. The resource is checked as declared where the\n * view is (parseListView), and its columns against the catalog (listProblem).\n */\nfunction parseCalendarBackdrop(raw: unknown, at: string): AppCalendarBackdrop[] {\n const list = Array.isArray(raw) ? raw : [raw];\n if (!list.length || list.length > CALENDAR_BACKDROPS_MAX) {\n throw new Error(\n `${at}: a calendar shades one to ${CALENDAR_BACKDROPS_MAX} kinds of span, like { from: 'blackout', start: 'starts_on', end: 'ends_on', labelColumn: 'name' }.`,\n );\n }\n return list.map((entry, i) => {\n const here = list.length > 1 ? `${at} ${i + 1}` : at;\n if (!isRecord(entry)) throw new Error(`${here} is not { from, start, end, labelColumn }.`);\n allowKeys(entry, ['from', 'start', 'end', 'labelColumn', 'tone'], here, BACKDROP_RENAMED);\n if (typeof entry.from !== 'string' || !entry.from.trim())\n throw new Error(`${here} has no \"from\": the resource whose rows are the spans, like 'blackout'.`);\n const start = localColumn(entry, 'start', here);\n const end = localColumn(entry, 'end', here);\n const labelColumn = localColumn(entry, 'labelColumn', here);\n if (entry.tone !== undefined && !(DISPLAY_TONES as readonly unknown[]).includes(entry.tone)) {\n throw new Error(`${here} has a \"tone\" of \"${String(entry.tone)}\". Use ${DISPLAY_TONES.join(', ')}.`);\n }\n return {\n from: entry.from.trim(),\n start,\n end,\n labelColumn,\n ...(entry.tone === undefined ? {} : { tone: entry.tone as AppCalendarBackdrop['tone'] }),\n };\n });\n}\n\n/**\n * How a list's rows draw: a checklist, a queue, a board — each a card of one\n * `primary` column and up to its `lines` — or a split list/detail workspace.\n * Local readable columns only for the cards, refused by name, so an author\n * learns one grammar.\n */\n/** Shared shape validation for authored lists and captured App embeds. */\nexport function parsePresentation(raw: unknown, at: string): AppListPresentation {\n const here = `${at}, presentation`;\n if (!isRecord(raw)) throw new Error(`${here} must be a checklist, queue, board, calendar, tree, gantt, cards, split or custom object.`);\n const kind = raw.kind;\n /* The author's own component draws the rows; the list around them is the platform's. */\n if (kind === 'custom') return parseCustomList(raw, `${here} (a custom list)`);\n if (\n kind !== 'checklist' &&\n kind !== 'queue' &&\n kind !== 'board' &&\n kind !== 'calendar' &&\n kind !== 'tree' &&\n kind !== 'gantt' &&\n kind !== 'cards' &&\n kind !== 'split'\n ) {\n throw new Error(`${here} needs a \"kind\" of \"checklist\", \"queue\", \"board\", \"calendar\", \"tree\", \"gantt\", \"cards\", \"split\" or \"custom\".`);\n }\n allowKeys(raw, PRESENTATION_KEYS[kind], `${here} (a ${kind})`, PRESENTATION_RENAMED[kind as keyof typeof PRESENTATION_RENAMED]);\n if (kind === 'split') return parseSplit(raw, `${here} (a split)`);\n if (kind === 'gantt') {\n const start = localColumn(raw, 'start', here);\n const end = localColumn(raw, 'end', here);\n if (end === start) throw new Error(`${here}: \"end\" names \"${end}\", the same column as \"start\"; a bar needs the column it ends on.`);\n return {\n kind,\n start,\n end,\n primary: queueColumn(raw.primary, 'primary', here),\n ...(raw.by === undefined ? {} : { by: queueColumn(raw.by, 'by', here) }),\n ...(raw.progress === undefined ? {} : { progress: localColumn(raw, 'progress', here) }),\n };\n }\n /* A card with no title of its own is titled by its record's name, `$label`, which resolving the view against\n the catalog makes the path that name is read from (appCards.ts cardsTitled). */\n /* A board, a queue or a calendar with no `primary` says the record's name too: resolving the list against the\n catalog reads it where the resource names its records (app.ts titledPresentation). */\n const primary =\n (kind === 'board' || kind === 'queue' || kind === 'calendar') && raw.primary === undefined\n ? LABEL_SEGMENT\n : kind === 'queue' || kind === 'calendar' || kind === 'tree' || kind === 'board'\n ? queueColumn(raw.primary, 'primary', here)\n : kind === 'cards' && raw.primary === undefined\n ? LABEL_SEGMENT\n : kind === 'cards'\n ? cardTitle(raw.primary, here)\n : localColumn(raw, 'primary', here);\n /* A card's captioned values are its `properties`; every other presentation's quiet lines under the title are `lines`.\n The model keeps one list for both. */\n const linesKey = kind === 'cards' ? 'properties' : 'lines';\n const rawLines = raw[linesKey];\n let lines: string[] = [];\n if (rawLines !== undefined) {\n const max = MAX_LINES[kind];\n if (!Array.isArray(rawLines) || rawLines.length > max) {\n throw new Error(`${here}: \"${linesKey}\" must be a list of at most ${max} local column${max === 1 ? '' : 's'} beneath the primary.`);\n }\n /* Cards, queues and calendars may say a referenced record's name; the\n other presentations' lines stay the row's own columns. */\n lines = rawLines.map((_, i) => {\n const line = (rawLines as unknown[])[i];\n if (kind === 'queue' || kind === 'calendar' || kind === 'tree') return queueColumn(line, String(i), `${here}, lines`);\n if (kind !== 'cards') return localColumn(rawLines as unknown as Record<string, unknown>, String(i), `${here}, lines`);\n if (typeof line !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*(\\.[A-Za-z_][A-Za-z0-9_]*)?$/.test(line.trim())) {\n throw new Error(`${here}, properties: a card's property is a local column or a one-hop label path like \"dealer_id.name\".`);\n }\n return line.trim();\n });\n }\n if (kind === 'checklist')\n return {\n kind,\n primary,\n checked: localColumn(raw, 'checked', here),\n ...(lines.length ? { lines } : {}),\n };\n if (kind === 'tree')\n return {\n kind,\n parent: localColumn(raw, 'parent', here),\n primary,\n ...(lines.length ? { lines } : {}),\n };\n if (kind === 'queue') {\n let facts: string[] | undefined;\n if (raw.properties !== undefined) {\n if (!Array.isArray(raw.properties) || !raw.properties.length || raw.properties.length > 6) {\n throw new Error(`${here}: \"properties\" must be one to 6 local columns or one-hop label paths.`);\n }\n facts = raw.properties.map((fact, i) => queueColumn(fact, String(i), `${here}, properties`));\n }\n const badge = raw.badge === undefined ? undefined : queueColumn(raw.badge, 'badge', here);\n const openLabel = raw.openLabel;\n if (openLabel !== undefined && (typeof openLabel !== 'string' || !openLabel.trim())) {\n throw new Error(`${here}: \"openLabel\" must be a non-empty string.`);\n }\n return {\n kind,\n primary,\n ...(lines.length ? { lines } : {}),\n ...(facts?.length ? { facts } : {}),\n ...(badge === undefined ? {} : { badge }),\n ...(typeof openLabel === 'string' ? { openLabel: openLabel.trim() } : {}),\n };\n }\n if (kind === 'calendar') {\n const date = localColumn(raw, 'date', here);\n /* A range: the record spans its days from `date` to `end`, both its own\n columns, so a move can shift the two together. */\n const end = raw.end === undefined ? undefined : localColumn(raw, 'end', here);\n if (end !== undefined && end === date)\n throw new Error(`${here}: \"end\" names \"${end}\", the same column as \"date\"; a range needs the column its last day is in.`);\n const badge = raw.badge === undefined ? undefined : queueColumn(raw.badge, 'badge', here);\n const backdrop = raw.backdrop === undefined ? undefined : parseCalendarBackdrop(raw.backdrop, `${here}, backdrop`);\n return {\n kind,\n date,\n ...(end === undefined ? {} : { end }),\n primary,\n ...(lines.length ? { lines } : {}),\n ...(badge === undefined ? {} : { badge }),\n ...(backdrop ? { backdrop } : {}),\n };\n }\n if (kind === 'cards') return parseCards(raw, `${here} (cards)`, primary, lines);\n const by = localColumn(raw, 'by', here);\n let lanes: string[] | undefined;\n if (raw.lanes !== undefined) {\n if (\n !Array.isArray(raw.lanes) ||\n !raw.lanes.length ||\n raw.lanes.length > 8 ||\n raw.lanes.some((lane) => typeof lane !== 'string' || !lane.trim())\n ) {\n throw new Error(`${here}: \"lanes\" must be one to eight non-empty lane values.`);\n }\n lanes = (raw.lanes as string[]).map((lane) => lane.trim());\n if (new Set(lanes).size !== lanes.length) throw new Error(`${here}: \"lanes\" must be unique.`);\n }\n const order = raw.order === undefined ? undefined : localColumn(raw, 'order', here);\n const swimlanes = raw.swimlanes === undefined ? undefined : localColumn(raw, 'swimlanes', here);\n /* A row, a lane and a place are three questions; one column cannot answer two of them. */\n if (swimlanes !== undefined && (swimlanes === by || swimlanes === order)) {\n throw new Error(\n `${here}: \"swimlanes\" names \"${swimlanes}\", which is already the board's \"${swimlanes === by ? 'by' : 'order'}\". Rows need a column of their own.`,\n );\n }\n let limits: Record<string, number> | undefined;\n if (raw.limits !== undefined) {\n if (!isRecord(raw.limits) || !Object.keys(raw.limits).length) {\n throw new Error(`${here}: \"limits\" must be an object of lane values to whole numbers, like { in_progress: 4 }.`);\n }\n limits = {};\n for (const [key, n] of Object.entries(raw.limits)) {\n const lane = key.trim();\n if (!lane) throw new Error(`${here}: \"limits\" has a blank lane value.`);\n if (lanes && !lanes.includes(lane))\n throw new Error(`${here}: \"limits\" names \"${lane}\", which is not one of its \"lanes\" (${lanes.join(', ')}).`);\n if (typeof n !== 'number' || !Number.isInteger(n) || n < 1 || n > 999) {\n throw new Error(`${here}: \"limits.${lane}\" must be a whole number from 1 to 999: the lane's work-in-progress limit.`);\n }\n limits[lane] = n;\n }\n }\n const category = raw.category === undefined ? undefined : localColumn(raw, 'category', here);\n if (category !== undefined && category === by)\n throw new Error(`${here}: \"category\" names \"${category}\", the board's \"by\": every card in a lane would wear the lane's chip.`);\n const footer = raw.footer === undefined ? undefined : parseBoardFooter(raw.footer, here);\n const sum = raw.sum === undefined ? undefined : localColumn(raw, 'sum', here);\n if (sum !== undefined && sum === by)\n throw new Error(`${here}: \"sum\" names \"${sum}\", the board's \"by\": a lane's own value is not an amount.`);\n let folded: string[] | undefined;\n if (raw.folded !== undefined) {\n if (\n !Array.isArray(raw.folded) ||\n !raw.folded.length ||\n raw.folded.length > 8 ||\n raw.folded.some((lane) => typeof lane !== 'string' || !lane.trim())\n )\n throw new Error(`${here}: \"folded\" must be one to eight lane values, like ['won', 'lost'].`);\n folded = (raw.folded as string[]).map((lane) => lane.trim());\n const stray = lanes && folded.find((lane) => !lanes!.includes(lane));\n if (stray) throw new Error(`${here}: \"folded\" names \"${stray}\", which is not one of its \"lanes\" (${lanes!.join(', ')}).`);\n }\n return {\n kind,\n by,\n ...(lanes ? { lanes } : {}),\n ...(order === undefined ? {} : { order }),\n ...(swimlanes === undefined ? {} : { swimlanes }),\n ...(limits ? { limits } : {}),\n primary,\n ...(lines.length ? { lines } : {}),\n ...(category === undefined ? {} : { category }),\n ...(footer ? { footer } : {}),\n ...(sum === undefined ? {} : { sum }),\n ...(folded ? { folded: [...new Set(folded)] } : {}),\n };\n}\n\n/** A card's foot: one to four local columns, each bare or `{ column, icon }`. */\nfunction parseBoardFooter(raw: unknown, here: string): AppBoardFact[] {\n if (!Array.isArray(raw) || !raw.length || raw.length > 4)\n throw new Error(`${here}: \"footer\" must be one to four local columns, each a name or { column, icon }.`);\n const facts = raw.map((item, index): AppBoardFact => {\n const at = `${here}, footer[${index}]`;\n if (typeof item === 'string') return { column: localColumn({ column: item }, 'column', at) };\n if (!isRecord(item)) throw new Error(`${at} must be a local column or { column, icon }.`);\n allowKeys(item, ['column', 'icon'], at);\n return {\n column: localColumn(item, 'column', at),\n ...(item.icon === undefined ? {} : { icon: parseIcon(item.icon, at) }),\n };\n });\n const seen = new Set<string>();\n for (const fact of facts) {\n if (seen.has(fact.column)) throw new Error(`${here}: \"footer\" names \"${fact.column}\" twice.`);\n seen.add(fact.column);\n }\n return facts;\n}\n\n/**\n * A split draws the view's own list — its show, query, tabs and filter bar —\n * beside the record the reader picks, so all it says itself is the detail's\n * fields and the list's page size. The detail is headed by the resource's\n * noun, as New is.\n */\nfunction parseSplit(input: Record<string, unknown>, at: string): AppSplitListDetail {\n const result: AppSplitListDetail = { kind: 'split' };\n if (input.detail !== undefined) {\n if (!Array.isArray(input.detail) || input.detail.length > 64) {\n throw new Error(`${at}: \"detail\" must be a list of at most 64 columns or label paths: the picked record's fields.`);\n }\n const detail = parseShow(input.detail, `${at}, detail`);\n if (new Set(detail).size !== detail.length) throw new Error(`${at}: \"detail\" names one column twice.`);\n result.detail = detail;\n }\n if (input.limit !== undefined) {\n if (!Number.isInteger(input.limit) || (input.limit as number) < 1 || (input.limit as number) > 200) {\n throw new Error(`${at}: \"limit\" must be an integer from 1 to 200: the list's page size.`);\n }\n result.limit = input.limit as number;\n }\n if (input.row !== undefined) {\n /* The queue's vocabulary: a split's rows are the queue's rows, narrowed to the pane beside a record. */\n const here = `${at}, \"row\"`;\n if (!isRecord(input.row)) throw new Error(`${here} must be an object: { primary, lines, properties, badge, avatar }.`);\n allowKeys(input.row, ['primary', 'lines', 'properties', 'badge', 'avatar'], here, PRESENTATION_RENAMED.queue);\n const row = input.row;\n const primary = queueColumn(row.primary, 'primary', here);\n if (row.lines !== undefined && (!Array.isArray(row.lines) || row.lines.length > MAX_LINES.queue)) {\n throw new Error(`${here}: \"lines\" must be a list of at most ${MAX_LINES.queue} columns beneath the primary.`);\n }\n const lines = ((row.lines ?? []) as unknown[]).map((line, i) => queueColumn(line, String(i), `${here}, lines`));\n if (row.properties !== undefined && (!Array.isArray(row.properties) || !row.properties.length || row.properties.length > 6)) {\n throw new Error(`${here}: \"properties\" must be one to 6 local columns or one-hop label paths.`);\n }\n const facts = ((row.properties ?? []) as unknown[]).map((fact, i) => queueColumn(fact, String(i), `${here}, properties`));\n result.row = {\n primary,\n ...(lines.length ? { lines } : {}),\n ...(facts.length ? { facts } : {}),\n ...(row.badge === undefined ? {} : { badge: queueColumn(row.badge, 'badge', here) }),\n ...(row.avatar === undefined ? {} : { avatar: queueColumn(row.avatar, 'avatar', here) }),\n };\n }\n return result;\n}\n\n/* ---------- dashboard views ---------- */\n\nfunction parseDashboardView(\n v: Record<string, unknown>,\n key: string,\n at: string,\n data: ParsedData,\n views: ReadonlyMap<string, ParsedView>,\n signsIn: boolean,\n): DashboardView {\n allowKeys(v, [...DASHBOARD_VIEW_KEYS, 'realm'], at, DASHBOARD_VIEW_RENAMED);\n if (typeof v.label !== 'string' || !v.label.trim()) throw new Error(`${at} is a dashboard and has no \"label\".`);\n /* A dashboard-level \"from\" is the sections' default — a board over one\n table should not repeat its name six times. A section that names its\n own still wins. */\n const description = optionalText(v, 'description', at);\n /* The words over the sections when they are not the menu's: \"Hello, {me.name}\". */\n /* false: the title is not drawn (it stays for a screen reader); the menu and the tab keep the label. */\n const heading = v.heading === false ? undefined : optionalText(v, 'heading', at);\n checkGreeting(heading, `${at}, \"heading\"`, signsIn);\n checkGreeting(description, `${at}, \"description\"`, signsIn);\n const table = optionalText(v, 'from', at);\n if (table) {\n checkResourceSlot(table, `${at}, \"from\"`, data);\n checkDeclaredResource(table, `${at}, \"from\"`, data);\n }\n if (\n v.windows !== undefined &&\n (!Array.isArray(v.windows) || !v.windows.length || v.windows.some((w) => typeof w !== 'string' || !w.trim()))\n ) {\n throw new Error(`${at} has \"windows\" that are not a list of phrases like \"last 30 days\".`);\n }\n /* The window it opens on, one of the windows it offers; an address's `?period=` still wins. */\n const offered = ((v.windows as string[] | undefined) ?? DEFAULT_WINDOWS).map((w) => w.trim().replace(/\\s+/g, ' '));\n const opensOn = typeof v.window === 'string' ? v.window.trim().replace(/\\s+/g, ' ') : undefined;\n if (v.window !== undefined && (!opensOn || !offered.includes(opensOn)))\n throw new Error(\n `${at} has a \"window\" of ${JSON.stringify(v.window)}, which is not one of the windows it offers: ${offered.map((w) => JSON.stringify(w)).join(', ')}.`,\n );\n if (v.windowControl !== undefined && v.windowControl !== 'segmented' && v.windowControl !== 'button')\n throw new Error(`${at} has a \"windowControl\" of ${JSON.stringify(v.windowControl)}. Use \"segmented\" or \"button\".`);\n if (v.filters !== undefined && (!Array.isArray(v.filters) || v.filters.some((f) => typeof f !== 'string' || !f.trim()))) {\n throw new Error(`${at} has \"filters\" that are not a list of column names.`);\n }\n if (!Array.isArray(v.sections)) throw new Error(`${at} needs \"sections\": a list of at least one section.`);\n if (v.aside !== undefined && !Array.isArray(v.aside)) throw new Error(`${at} has an \"aside\" that is not a list of sections.`);\n const embeds: string[] = [];\n const sections = parseDashSections(v.sections, at, table, data, views, embeds);\n const aside = Array.isArray(v.aside) ? parseDashSections(v.aside, `${at}, \"aside\"`, table, data, views, embeds, true) : undefined;\n nameSectionWindows([...sections, ...(aside ?? [])], at);\n const areas = parseAreas(v.areas, at, sections, aside !== undefined);\n /* The sections' default is one only while a section inherits it: a table\n every section names again for itself is read by nothing. */\n if (\n table &&\n ![...(v.sections as unknown[]), ...((v.aside as unknown[] | undefined) ?? [])].some(\n (s) =>\n isRecord(s) &&\n s.kind !== 'note' &&\n s.kind !== 'view' &&\n s.kind !== 'firstRun' &&\n s.kind !== 'activity' &&\n s.kind !== 'find' &&\n s.kind !== 'branding' &&\n s.kind !== 'integrations' &&\n s.from === undefined,\n )\n ) {\n throw new Error(`${at} has a \"from\" that no section inherits: every section names its own \"from\". Remove it.`);\n }\n return {\n kind: 'dashboard',\n key,\n at,\n embeds,\n tables: [...new Set([...sections, ...(aside ?? [])].map((section) => section.table).filter((name): name is string => Boolean(name)))],\n view: {\n label: v.label.trim(),\n ...(heading === undefined ? {} : { heading }),\n ...(v.heading === false ? { headingHidden: true as const } : {}),\n ...(description === undefined ? {} : { description }),\n sections,\n ...(aside === undefined ? {} : { aside }),\n ...(areas === undefined ? {} : { areas }),\n ...(v.path === undefined ? {} : { path: parseRoutePath(v.path, at) }),\n ...(v.icon === undefined ? {} : { icon: parseIcon(v.icon, at) }),\n ...(v.realm === undefined ? {} : { realm: parseRealm(v.realm, at) }),\n ...(v.windows === undefined ? {} : { windows: (v.windows as string[]).map((w) => w.trim()) }),\n ...(opensOn === undefined ? {} : { window: opensOn }),\n ...(v.windowControl === 'button' ? { windowControl: 'button' as const } : {}),\n ...(v.filters === undefined ? {} : { filters: (v.filters as string[]).map((f) => f.trim()) }),\n ...(favoriteFlag(v.favorite, at) ? { favorite: true as const } : {}),\n },\n };\n}\n\n/**\n * The keys of a free-form page as the App loader writes it from a\n * `pages/*.page.tsx` file (packages/mistri/src/server/app/appPages.ts) — never authored in\n * `views`, which refuses the kind. `heading` and `props` are the internal\n * Console's, whose pages are written as data.\n */\nconst PAGE_VIEW_KEYS = [\n 'kind',\n 'label',\n 'icon',\n 'description',\n 'heading',\n 'favorite',\n 'component',\n 'reads',\n 'commands',\n 'params',\n 'loader',\n 'props',\n 'roles',\n 'public',\n] as const;\n\n/**\n * A free-form page: the whole page drawn by the author's component. It is\n * served as a dashboard of one full-width custom section — so its menu\n * entry, its home rule, its address and its favorite star are a\n * dashboard's, with nothing to learn twice — which says `page`: the\n * resources it reads, which gate it like a section's table gates a\n * dashboard (a reader who cannot read one is not served the page), the\n * commands it runs, the address parameters it keeps its place in and the\n * loader that reads for it on the server.\n *\n * Who is offered it is said once, here, by what it reads: every role a page\n * names must be granted every read, or the page is a mistake some role would\n * meet broken; one that names none is offered to the people's roles that\n * are — never a service role, which no person holds — and never to a\n * signed-out visitor unless it says `public: true`, when every read must be\n * one `auth.public` opens. A command is not a door: a reader the page is\n * offered to who may not run one of its commands meets the page without it\n * (the command's handle says `granted: false`, and the server refuses it as\n * it refuses any command not granted). A command no role offered the page\n * may run is a mistake, as an unreadable read is.\n */\nfunction parsePageView(v: Record<string, unknown>, key: string, at: string, ctx: ContentContext): DashboardView {\n allowKeys(v, PAGE_VIEW_KEYS, `${at} (a free-form page)`);\n if (typeof v.label !== 'string' || !v.label.trim()) throw new Error(`[page-file] ${at} is a free-form page and has no \"title\".`);\n const page = parsePage(v, at);\n for (const resource of page.reads) {\n checkResourceSlot(resource, `${at}, \"reads\"`, ctx.data);\n checkDeclaredResource(resource, `${at}, \"reads\"`, ctx.data);\n }\n for (const command of page.commands) {\n if (!isRegisteredCommand(command)) {\n throw new Error(`[page-commands] ${at} runs the command \"${command}\", which the commands module does not register.`);\n }\n }\n if (v.public !== undefined && v.public !== true) throw new Error(`[page-public] ${at} has a \"public\" that is not true.`);\n /* A public page's commands are run by a signed-out visitor through the page's own door (http/pageCommandRoutes.ts),\n as the public principal: only a command that is itself public, and one that makes its record rather than naming\n one, the way a public form's is. A signed-in page's need roles to be granted to. */\n if (v.public === true) {\n for (const command of page.commands) {\n const entry = registeredCommand(command);\n if (entry?.public !== true)\n throw new Error(\n `[page-commands] ${at} is public and runs \"${command}\", which is not public: a public page runs only commands that say public: true.`,\n );\n if (entry.targets !== 'none')\n throw new Error(`[page-commands] ${at} is public and runs \"${command}\", which names a record; a public command makes its own.`);\n }\n if (page.commands.length && !ctx.auth)\n throw new Error(`[page-commands] ${at} runs public commands, and a public command needs \"auth\" (with \"auth.public\").`);\n } else if (page.commands.length && !ctx.auth?.roles)\n throw new Error(`[page-commands] ${at} runs commands, and commands need \"auth\" and \"auth.roles\".`);\n const roles = pageRoles(v, page, at, ctx);\n const description = optionalText(v, 'description', at);\n if (v.heading !== undefined && v.heading !== false) throw new Error(`${at}: a page's \"heading\" is false, to draw it without its title.`);\n return {\n kind: 'dashboard',\n key,\n at,\n embeds: [],\n tables: page.reads,\n view: {\n ...(v.heading === false ? { headingHidden: true as const } : {}),\n label: v.label.trim(),\n ...(description === undefined ? {} : { description }),\n sections: [\n {\n kind: 'custom',\n component: page.component,\n props: page.props,\n width: 'full',\n },\n ],\n page: {\n reads: page.reads,\n commands: page.commands,\n params: page.params,\n ...(page.loader ? { loader: page.loader } : {}),\n ...(v.public === true ? { public: true as const } : {}),\n },\n ...(v.icon === undefined ? {} : { icon: parseIcon(v.icon, at) }),\n ...(favoriteFlag(v.favorite, at) ? { favorite: true as const } : {}),\n ...(roles ? { roles } : {}),\n },\n };\n}\n\n/**\n * The roles a free-form page is offered to. Named, each must be granted\n * what the page reads (`page-coverage`); unnamed, the roles that are, but\n * a service role. Either way each command it runs must be some offered\n * role's. A public page names none: it is everyone's, a signed-out\n * visitor's too, and reads only what `auth.public` opens.\n */\nfunction pageRoles(\n v: Record<string, unknown>,\n page: { reads: string[]; commands: string[] },\n at: string,\n ctx: ContentContext,\n): string[] | undefined {\n const grants = ctx.auth?.roles;\n if (v.public === true) {\n if (v.roles !== undefined) throw new Error(`[page-public] ${at} is public and names \"roles\": a public page is everyone's.`);\n const open = new Set(ctx.auth?.public?.read ?? []);\n if (!ctx.auth?.public)\n throw new Error(`[page-public] ${at} is public, and the App opens nothing to signed-out visitors (auth.public).`);\n const closed = page.reads.find((resource) => !open.has(resource));\n if (closed) throw new Error(`[page-public] ${at} is public and reads \"${closed}\", which \"auth.public\" does not open.`);\n return undefined;\n }\n if (!grants) return parseOpenRoles(v.roles, at, ctx.auth);\n const reads = (grant: AppRoleGrant, resource: string) =>\n grant.read === true || (Array.isArray(grant.read) && grant.read.includes(resource));\n const unread = (grant: AppRoleGrant): string | undefined => page.reads.find((resource) => !reads(grant, resource));\n /* Each command is some offered role's to run, or no one could: a mistake, as an unreadable read is. */\n const runnable = (offered: readonly string[]) => {\n const stray = page.commands.find((command) => !offered.some((role) => (grants[role].commands ?? []).includes(command)));\n if (stray !== undefined) {\n throw new Error(\n `[page-coverage] ${at} runs \"${stray}\", which no role it is offered to is granted. Grant it to one, or leave it out of \"commands\".`,\n );\n }\n };\n const named = parseOpenRoles(v.roles, at, ctx.auth);\n if (named) {\n for (const role of named) {\n const missing = unread(grants[role]);\n if (missing !== undefined) {\n throw new Error(\n `[page-coverage] ${at} is offered to the role \"${role}\", which is not granted \"${missing}\". ` +\n 'Every role a page names reads all it reads: grant it, or leave the role out.',\n );\n }\n }\n runnable(named);\n return named;\n }\n const able = Object.keys(grants).filter((role) => !grants[role].service && unread(grants[role]) === undefined);\n if (!able.length) {\n throw new Error(\n `[page-coverage] ${at} reads what no person's role is granted all of, so no one could open it. Grant a role, or narrow the page.`,\n );\n }\n runnable(able);\n return able;\n}\n\n/** A view with the roles it names, checked against the App's (appViewRoles.ts). */\nfunction withOpenRoles<V extends object>(view: V, raw: Record<string, unknown>, at: string, ctx: ContentContext): V & { roles?: string[] } {\n const roles = parseOpenRoles(raw.roles, at, ctx.auth);\n return roles ? { ...view, roles } : view;\n}\n\n/** A section kind as a noun: \"a metric\", \"a rows section\" — \"a rows\" is not English. */\n/** Keys a dashboard section kind took under another name, with what they are now. */\nconst RENAMED_SECTION_KEYS: Record<string, Record<string, string>> = {\n metric: { view: '\"to\": metric(title, from, { to: \\'overdue_list\\' }) opens the list view it names' },\n custom: {\n totals: '\"figures\": the named aggregate reads its component receives as figures.<name> (\"totals\" is a list\\'s footer of sums)',\n },\n};\n\nconst sectionWord = (kind: string) => (kind === 'rows' ? 'rows section' : kind);\n\n/** The target's standing filter stays ahead of a metric's own refinement,\n both when parsed and when its compare/window is derived (app.ts). */\nexport function metricCohortQuery(standingQuery: string | undefined, refinement: string | undefined): string | undefined {\n const standing = andQuery(splitClauseTail(standingQuery ?? '').head);\n const own = andQuery(splitClauseTail(refinement ?? '').head);\n // A normalized section may be parsed again. Only a whole leading\n // conjunction counts as already scoped; an `or` is bracketed above.\n return (standing && own !== standing && !own.startsWith(`${standing} and `) ? andQuery(standing, own) : own) || undefined;\n}\n\nfunction parseDashSections(\n raw: unknown[],\n at: string,\n defaultTable: string | undefined,\n data: ParsedData,\n views: ReadonlyMap<string, ParsedView>,\n embeds: string[],\n aside = false,\n): AppDashSection[] {\n if (!raw.length) throw new Error(`${at}: a dashboard needs at least one section.`);\n /* Each section is judged on its own: one run names every section's refusal (appProblems.ts). */\n const problems: string[] = [];\n const sections = raw.map((entry, i) =>\n gather(problems, (): AppDashSection => {\n const here = `${at}, section ${i + 1}`;\n if (!isRecord(entry)) throw new Error(`${here} is not an object.`);\n const s = entry;\n if (s.kind === 'view') {\n const section = embedSection(s, here, views, data, aside);\n embeds.push(s.view as string);\n return section;\n }\n const kind = String(s.kind ?? '');\n if (!(DASH_KINDS as readonly string[]).includes(kind)) {\n throw new Error(\n `${here} has an unknown kind \"${kind}\". Use \"metric\", \"breakdown\", \"rows\", \"chart\", \"note\", \"matrix\", \"funnel\", \"board\", \"firstRun\", \"custom\", \"activity\", \"find\", \"branding\", \"integrations\" or \"view\", each written with its helper: metric(), breakdown(), rows(), chart(), note(), matrix(), funnel(), board(), firstRun(), custom(), activity(), find(), brandingForm(), integrationsForm(), embed().`,\n );\n }\n if (kind === 'firstRun') {\n if (aside) throw new Error(`${here} is a first run, which stands in the dashboard's main flow: move it into \"sections\".`);\n if (raw.filter((other) => isRecord(other) && other.kind === 'firstRun').length > 1) {\n throw new Error(`${at} has more than one first run. One list of first steps is the start; put every step in it.`);\n }\n return firstRunSection(s, here, views);\n }\n /* Per kind, so `show` on a metric or `over` on a note is refused rather\n than read by nothing — `over` on a note used to switch the whole\n dashboard's window controls on for a section the window never reached.\n A key another kind reads is named as that kind's (\"only a chart\n carries\"); a key no kind reads is a typo and gets the list. */\n const takes = SECTION_KEYS[kind as AppDashSection['kind']];\n for (const key of Object.keys(s)) {\n if (takes.includes(key) || /^[$_]/.test(key)) continue;\n /* A key renamed within its own kind is told as the rename: \"only a view carries\" for a metric's `view` sent the\n author to the embed, when the card's destination is `to`. */\n const renamed = RENAMED_SECTION_KEYS[kind]?.[key];\n if (renamed) throw new Error(`${here} has \"${key}\", which is now ${renamed}.`);\n const owners = (Object.keys(SECTION_KEYS) as Array<AppDashSection['kind']>).filter((k) => SECTION_KEYS[k].includes(key));\n if (owners.length) throw new Error(`${here} has \"${key}\", which only a ${owners.map(sectionWord).join(' or ')} carries.`);\n throw new Error(`${here} (a ${sectionWord(kind)}) ${unknownKey(key, takes, keyHomesOf(key))}.`);\n }\n if (kind === 'custom') return parseCustomDashSection(s, here, data);\n if (kind === 'activity') return parseActivitySection(s, here, data);\n if (kind === 'find') return parseFindSection(s, here, data);\n /* The tenant's own Branding form: what it may change is the App's `branding` (checked at the root). The\n integrations' settings form is the same shape: what it shows is the jobs module's, read from the server. */\n if (kind === 'branding' || kind === 'integrations') {\n if (s.title !== undefined && (typeof s.title !== 'string' || !s.title.trim()))\n throw new Error(`${here} has a \"title\" that is not text.`);\n if (s.width !== undefined && !WIDTHS.includes(String(s.width) as SectionWidth))\n throw new Error(`${here} has a \"width\" of \"${String(s.width)}\". Use ${WIDTHS.join(', ')}.`);\n return {\n kind,\n ...(s.title === undefined ? {} : { title: (s.title as string).trim() }),\n ...(s.icon === undefined ? {} : { icon: parseIcon(s.icon, here) }),\n ...(s.width === undefined ? {} : { width: s.width as SectionWidth }),\n };\n }\n let banner: Pick<AppDashSection, 'variant' | 'eyebrow' | 'image'> = {};\n if (kind === 'note') {\n if (typeof s.text !== 'string' || !s.text.trim()) throw new Error(`${here} is a note and has no \"text\".`);\n if (s.action !== undefined) {\n parseAppLinkAction(s.action, `${here}, \"action\"`);\n }\n banner = parseNoteBanner(s, here, data);\n } else if (s.from === undefined ? !defaultTable : typeof s.from !== 'string' || !s.from.trim()) {\n throw new Error(\n `${here} has no \"from\"${\n defaultTable === undefined ? ' — name one, or set a dashboard-level \"from\" for every section to inherit' : ''\n }.`,\n );\n }\n if (typeof s.from === 'string') {\n checkResourceSlot(s.from.trim(), `${here}, \"from\"`, data);\n checkDeclaredResource(s.from.trim(), `${here}, \"from\"`, data);\n }\n if ((kind === 'breakdown' || kind === 'chart') && (typeof s.by !== 'string' || !s.by.trim())) {\n throw new Error(`${here} has no \"by\" — the column its groups are counted under.`);\n }\n let card: string[] | undefined;\n if (kind === 'board') {\n /* Lanes come from values of one of the table's own columns; a\n bucketed or dotted by has no value a card could sit under. */\n if (typeof s.by !== 'string' || !s.by.trim())\n throw new Error(`${here} is a board and has no \"by\" — the column whose values are its lanes.`);\n if (/[\\s.,]/.test(s.by.trim())) {\n throw new Error(\n `${here} has a \"by\" of \"${String(s.by).trim()}\" — a board's lanes come from a bare column of its own table, like \"status\".`,\n );\n }\n if (s.lanes !== undefined) {\n if (\n !Array.isArray(s.lanes) ||\n s.lanes.length < 1 ||\n s.lanes.length > 8 ||\n s.lanes.some((lane) => typeof lane !== 'string' || !lane.trim())\n ) {\n throw new Error(`${here} has \"lanes\" that are not a list of 1 to 8 nonblank strings.`);\n }\n if (new Set(s.lanes).size !== s.lanes.length)\n throw new Error(`${here} has duplicate \"lanes\". Each exact lane value may appear only once.`);\n }\n /* The one card vocabulary: a primary line, then its lines, label\n paths welcome. Absent, the card is the table's label column alone;\n lines beneath an unnamed primary would take its place. */\n if (s.primary !== undefined && (typeof s.primary !== 'string' || !s.primary.trim()))\n throw new Error(`${here} has a \"primary\" that is not a column or a label path.`);\n if (s.lines !== undefined && (!Array.isArray(s.lines) || s.lines.some((line) => typeof line !== 'string' || !line.trim()))) {\n throw new Error(`${here} has \"lines\" that are not a list of columns or label paths.`);\n }\n const lines = ((s.lines ?? []) as string[]).map((line) => line.trim());\n if (lines.length && s.primary === undefined) {\n throw new Error(\n `${here} has \"lines\" and no \"primary\": name the card's first line — the label column stands alone only on a card with no lines.`,\n );\n }\n if (s.primary !== undefined) card = [(s.primary as string).trim(), ...lines];\n }\n if (kind === 'matrix' && (typeof s.by !== 'string' || !s.by.trim() || !s.by.includes(','))) {\n throw new Error(\n `${here} is a matrix and needs a two-key \"by\" — \"stage, month close_date\": rows by the first, columns by the second.`,\n );\n }\n if (s.shape === 'funnel')\n throw new Error(\n `${here} has shape: 'funnel', which is now funnel(title, from, stages): ordered stages counted in one statement. Where rows stand now, by one column's values, is breakdown(title, from, column, { shape: 'steps' }).`,\n );\n const stages = kind === 'funnel' ? parseFunnelStages(s, here) : undefined;\n if (kind === 'breakdown') parseStepsShape(s, here);\n else if (s.shape !== undefined && !(DASH_CHART_SHAPES as readonly string[]).includes(String(s.shape))) {\n throw new Error(\n `${here} has a \"shape\" of \"${String(s.shape)}\". Use \"line\", \"bar\", \"donut\", \"stacked\", \"scatter\", \"heatmap\", \"waterfall\" or \"treemap\".`,\n );\n }\n if (kind === 'chart') parseChartShape(s, here);\n /* `auto` is a chart's word (autoBucket.ts): a breakdown lists its groups and a matrix crosses two fixed keys, and\n neither can have its unit move with the window; a heatmap's grid is a fixed grid of cells. */\n if (typeof s.by === 'string' && /(^|,)\\s*auto\\b/i.test(s.by)) {\n if (kind === 'matrix' || s.shape === 'heatmap') {\n throw new Error(\n `${here} is a ${kind === 'matrix' ? 'matrix' : 'heatmap'}, whose keys must both be fixed: \"auto\" picks a unit from the window, and a grid's cells cannot move with it. Write the unit, like \"month close_date\".`,\n );\n }\n if (kind !== 'chart') {\n throw new Error(\n `${here} has an \"auto\" key, which only a chart takes: its unit follows the window. Write the unit, like \"month close_date\".`,\n );\n }\n if (!autoKeyOf(s.by)) throw new Error(`${here} has a \"by\" of \"${s.by.trim()}\": \"auto\" cuts a date column, like \"auto created_at\".`);\n }\n if (s.shape === 'stacked' && (typeof s.by !== 'string' || !s.by.trim() || !s.by.includes(','))) {\n throw new Error(`${here} is a stacked chart and needs a two-key \"by\" — \"region, stage\": a bar for each region, divided by stage.`);\n }\n /* A goal a line or a bar can be measured against; a donut has no scale to rule it on. */\n /* A metric's goal may be read from data — a measure over a table and a query, in the metric's own words;\n a chart's rule is a number. */\n let target: number | DashTarget | undefined;\n /* A breakdown's goal is a row per group (G76): a seller's won against their quota, the target table's `by`\n matching each group's key, windowed by its own `over` as the section is by its. */\n const grouped = kind === 'breakdown';\n if (grouped && s.target !== undefined && (s.target === null || typeof s.target !== 'object' || Array.isArray(s.target)))\n throw new Error(\n `${here} has a \"target\" that is not a goal per group: { from, by, measure, over? }, a quota's amount by its owner.`,\n );\n if (grouped && s.target !== undefined && s.shape === 'steps')\n throw new Error(`${here} is drawn as steps, which have no bar to measure a \"target\" on.`);\n if (\n s.target !== undefined &&\n (kind === 'metric' || grouped) &&\n s.target !== null &&\n typeof s.target === 'object' &&\n !Array.isArray(s.target)\n ) {\n const t = s.target as Record<string, unknown>;\n const keys = grouped ? ['from', 'query', 'measure', 'by', 'over'] : ['from', 'query', 'measure'];\n if (t.table !== undefined)\n throw new Error(`${here} has a \"target\" with \"table\", which is now \"from\": the resource its goal is read from.`);\n const unknown = Object.keys(t).filter((key) => !keys.includes(key));\n if (unknown.length)\n throw new Error(\n grouped\n ? `${here} has a \"target\" with ${unknown.map((key) => `\"${key}\"`).join(', ')}. A breakdown's target says \"from\", \"by\", \"measure\", \"query\" and \"over\".`\n : `${here} has a \"target\" with ${unknown.map((key) => `\"${key}\"`).join(', ')}. A target from data says \"from\", \"query\" and \"measure\", as the metric does.`,\n );\n for (const key of keys) {\n if (t[key] !== undefined && (typeof t[key] !== 'string' || !(t[key] as string).trim()))\n throw new Error(`${here} has a \"target\" whose \"${key}\" is not a non-empty string.`);\n }\n if (grouped && (t.by === undefined || /\\s|,/.test((t.by as string).trim())))\n throw new Error(`${here} has a \"target\" with no single \"by\": the column of its table each group's key is matched on.`);\n if (grouped && typeof s.by === 'string' && /\\s|,/.test(s.by.trim()))\n throw new Error(`${here} groups by \"${s.by.trim()}\"; a target is matched on one plain column's value.`);\n if (t.query === undefined && t.measure === undefined && t.from === undefined)\n throw new Error(`${here} has an empty \"target\": give a number, or the \"from\", \"query\" and \"measure\" its goal is read from.`);\n if (t.from !== undefined) checkDeclaredResource((t.from as string).trim(), `${here}, \"target\", \"from\"`, data);\n if (typeof t.query === 'string') conditionsOnly(t.query, here, 'target');\n target = {\n table: (t.from as string | undefined)?.trim() ?? (typeof s.from === 'string' ? s.from.trim() : defaultTable) ?? '',\n ...(t.query === undefined ? {} : { query: (t.query as string).trim() }),\n measure: ((t.measure ?? s.measure ?? 'count') as string).trim(),\n ...(t.by === undefined ? {} : { by: (t.by as string).trim() }),\n ...(t.over === undefined ? {} : { over: (t.over as string).trim() }),\n };\n } else if (s.target !== undefined) {\n if (typeof s.target !== 'number' || !Number.isFinite(s.target)) {\n throw new Error(\n `${here} has a \"target\" that is not a number${kind === 'metric' ? ', or the \"from\", \"query\" and \"measure\" its goal is read from' : ''}.`,\n );\n }\n target = s.target;\n }\n if (s.curve !== undefined) {\n if (s.curve !== 'smooth' && s.curve !== 'straight')\n throw new Error(`${here} has a \"curve\" of ${JSON.stringify(s.curve)}. Use \"smooth\" or \"straight\".`);\n if (s.shape !== undefined && s.shape !== 'line')\n throw new Error(`${here} is a ${String(s.shape)} chart; only a line has a \"curve\".`);\n }\n if (s.total !== undefined) {\n if (s.shape !== 'donut') throw new Error(`${here} has a \"total\", which names a donut's middle figure; it is not a donut.`);\n if (typeof s.total !== 'string' || !s.total.trim() || s.total.trim().length > 40)\n throw new Error(`${here} has a \"total\" that is not a few words (at most 40 characters): what the middle figure counts.`);\n }\n if (s.target !== undefined && s.shape === 'donut')\n throw new Error(`${here} is a donut, which has no scale to draw a \"target\" on. Use a line or stacked bars.`);\n for (const key of ['title', 'query', 'measure', 'x', 'compare', 'by', 'sort', 'over'] as const) {\n if (s[key] !== undefined && (typeof s[key] !== 'string' || !(s[key] as string).trim())) {\n throw new Error(`${here} has a \"${key}\" that is not a non-empty string.`);\n }\n }\n for (const key of ['query', 'compare'] as const) {\n if (typeof s[key] === 'string' && s[key] !== PREVIOUS_PERIOD) conditionsOnly(s[key] as string, here, key);\n }\n const table = kind === 'note' ? undefined : typeof s.from === 'string' ? s.from.trim() : defaultTable;\n const columns = s.show === undefined ? undefined : parseShow(s.show, here);\n const labels = labelsFor(columns, table ? data.resources?.[table]?.labels : undefined);\n if (s.limit !== undefined && (typeof s.limit !== 'number' || !Number.isInteger(s.limit) || s.limit < 1)) {\n throw new Error(`${here} has a \"limit\" that is not a positive whole number.`);\n }\n if (s.height !== undefined && (typeof s.height !== 'number' || !Number.isFinite(s.height) || s.height < 120)) {\n throw new Error(`${here} has a \"height\" that is not a number of pixels (120 or more).`);\n }\n if (s.width !== undefined && !WIDTHS.includes(String(s.width) as SectionWidth)) {\n throw new Error(`${here} has a \"width\" of \"${String(s.width)}\". Use ${WIDTHS.join(', ')}.`);\n }\n if (s.better !== undefined) {\n if (!(BETTER as readonly string[]).includes(String(s.better)))\n throw new Error(`${here} has a \"better\" of \"${String(s.better)}\". Use \"up\" or \"down\".`);\n /* Without \"compare\" there is no change to colour: advice (appAdvisories), not a refusal. */\n }\n if (s.spark !== undefined && typeof s.spark !== 'boolean') throw new Error(`${here} has a \"spark\" that is not true or false.`);\n if (s.format !== undefined && !(FIGURE_FORMATS as readonly string[]).includes(String(s.format)))\n throw new Error(`${here} has a \"format\" of \"${String(s.format)}\". Use ${FIGURE_FORMAT_WORDS}.`);\n if (s.unit !== undefined && (typeof s.unit !== 'string' || !s.unit.trim() || s.unit.length > 24))\n throw new Error(`${here} has a \"unit\" that is not a short word said after the figure, like \"days\".`);\n if (s.figure !== undefined && s.figure !== 'exact' && s.figure !== 'compact')\n throw new Error(\n `${here} has a \"figure\" of ${JSON.stringify(s.figure)}. Use \"exact\" (to the cent, the default) or \"compact\" (\"$28.1K\").`,\n );\n const windows = parseSectionWindows(s, here);\n /* A metric counts its own query. The list it opens (`to:`) adds only its\n standing conditions — the rows that list can show at all, so the drill\n holds every row counted — never a tab: a tab is where a reader starts,\n not what the list is, and a metric with no query counted the first\n tab's open rows where it meant every row. The drill opens\n the list on exactly the counted rows (its \"All\" tab, where it has one).\n List plumbing is not part of a measure. Resolve this before windows and\n comparisons are derived, so every read and the drill share a cohort. */\n let opens: string | undefined;\n let query = s.query === undefined ? undefined : (s.query as string).trim();\n let compare = s.compare === undefined ? undefined : (s.compare as string).trim();\n /* A breakdown, chart or matrix says where its groups open the same way (`to: 'view-id'`, or `false` for groups that\n are text); a metric's card is the one doorway of its own. A group's list is judged as a metric's is. */\n const drills = kind === 'breakdown' || kind === 'chart' || kind === 'matrix' || kind === 'funnel';\n /* A metric's card takes false the same way: a figure read where it stands, opening nothing. */\n const doorway = drills || kind === 'metric';\n if (doorway && s.to !== undefined && s.to !== false && typeof s.to !== 'string')\n throw new Error(`${here} has a \"to\" that is not a list view's key or false.`);\n if ((kind === 'metric' || drills) && s.to !== undefined && s.to !== false) {\n const target = typeof s.to === 'string' ? views.get(s.to.trim()) : undefined;\n if (!target || target.kind !== 'list') {\n const lists = [...views.values()].filter((view) => view.kind === 'list').map((view) => view.key);\n throw new Error(\n `${here} opens \"${String(s.to)}\", which is not a list view this App declares. Its list views: ${lists.join(', ') || 'none'}.`,\n );\n }\n if (table !== undefined && target.view.table !== table) {\n throw new Error(\n `${here} counts ${table} and opens \"${target.key}\", a list of ${target.view.table}. ${drills ? 'A group opens a list of the rows it counted.' : 'A metric opens a list of the rows it counted.'}`,\n );\n }\n // An authored clause-only query must not become absent: parsing that\n // section again would silently count every row instead.\n for (const [key, value] of [\n ['query', query],\n ['compare', compare],\n ] as const) {\n if (value !== undefined && !splitClauseTail(value).head) {\n throw new Error(\n `${here} has a \"${key}\" with no filter conditions. A metric opening a list needs conditions, not only sort, show or limit clauses. ` +\n (key === 'query'\n ? 'Omit \"query\" to count every row the target view shows.'\n : 'Omit \"compare\" to leave out the comparison.'),\n );\n }\n }\n opens = target.key;\n query = metricCohortQuery(target.view.query, query);\n // A full compare is its own refinement; previous period is derived\n // later from the effective query.\n if (compare && compare !== PREVIOUS_PERIOD) compare = metricCohortQuery(target.view.query, compare);\n }\n /* The aside is a narrow column: rows there read as names, each with a\n quiet line under it and a value at its end — never a table's columns. */\n if (aside && kind === 'rows' && columns && columns.length > 3) {\n throw new Error(\n `${here} shows ${columns.length} columns. Rows in the aside draw as a name, a line under it and a value at its end: show at most three, or move them into \"sections\".`,\n );\n }\n /* What a section with nothing does: a rows section says so in place (guidance) or leaves itself out; the others\n leave themselves out. A dashboard has no 'collapse': the sections do not sit in a run of empty lists. */\n if (s.empty === false) throw new Error(`${here} has \"empty: false\", which is now \"empty: 'hide'\".`);\n if (s.empty === 'collapse')\n throw new Error(\n `${here} has \"empty: 'collapse'\", which only a record page's related lists take. A dashboard section that has nothing says so in place (rows take guidance) or is left out with \"empty: 'hide'\".`,\n );\n if (s.empty !== undefined && s.empty !== 'hide' && kind !== 'rows')\n throw new Error(\n `${here} (a ${sectionWord(kind)}) takes \"empty: 'hide'\", which leaves it out while it has nothing; guidance is a rows section's.`,\n );\n const emptyMode = s.empty === 'hide' ? ('hide' as const) : undefined;\n return {\n ...(opens === undefined ? {} : { view: opens }),\n ...(doorway && s.to === false ? { noDrill: true as const } : {}),\n kind: kind as AppDashSection['kind'],\n title: s.title === undefined ? undefined : String(s.title).trim(),\n table,\n text: s.text === undefined ? undefined : (s.text as string).trim(),\n /* A section's mark: beside its title, or a note's over its guidance. */\n ...(s.icon !== undefined ? { icon: parseIcon(s.icon, here) } : {}),\n ...(kind === 'note' && s.action !== undefined ? { action: parseAppLinkAction(s.action, `${here}, \"action\"`) } : {}),\n ...banner,\n query,\n measure: s.measure === undefined ? undefined : (s.measure as string).trim(),\n compare,\n better: s.better as AppDashSection['better'],\n spark: s.spark as boolean | undefined,\n ...(s.format === undefined ? {} : { format: s.format as AppDashSection['format'] }),\n ...(s.unit === undefined ? {} : { unit: (s.unit as string).trim() }),\n ...(s.figure === undefined ? {} : { figure: s.figure as 'exact' | 'compact' }),\n by: s.by === undefined ? undefined : (s.by as string).trim(),\n ...(stages ? { stages } : {}),\n ...(s.lanes === undefined ? {} : { lanes: [...(s.lanes as string[])] }),\n shape: s.shape as AppDashSection['shape'],\n ...(typeof s.x === 'string' ? { x: s.x.trim() } : {}),\n ...(s.curve === undefined ? {} : { curve: s.curve as 'smooth' | 'straight' }),\n ...(typeof s.total === 'string' ? { total: s.total.trim() } : {}),\n ...(s.notes === undefined\n ? {}\n : {\n notes: Object.fromEntries(Object.entries(s.notes as Record<string, string>).map(([k, v]) => [k, v.trim()])),\n }),\n over: s.over === undefined ? undefined : (s.over as string).trim(),\n ...(windows ? { windows } : {}),\n show: card ?? columns,\n ...(labels ? { labels } : {}),\n sort: s.sort === undefined ? undefined : (s.sort as string).trim(),\n ...(s.sizing === undefined ? {} : { sizing: parseTableSizing(s.sizing, here) }),\n ...(kind === 'rows' && s.empty !== undefined && !emptyMode ? { empty: parseListEmpty(s.empty, here) } : {}),\n ...(emptyMode ? { emptyMode } : {}),\n limit: s.limit as number | undefined,\n width: s.width as SectionWidth | undefined,\n ...(s.height === undefined ? {} : { height: s.height as number }),\n ...(target === undefined ? {} : { target }),\n };\n }),\n );\n /* Where a section stands on the layout is read beside its kind's keys, since every kind takes it the same way. */\n raw.forEach((entry, i) => {\n if (!isRecord(entry) || entry.area === undefined) return;\n gather(problems, () => {\n const here = `${at}, section ${i + 1}`;\n if (aside) throw new Error(`${here} has an \"area\", and the aside is not on the areas: an aside is a column of its own.`);\n if (entry.kind === 'firstRun')\n throw new Error(`${here} is a first run, which leads the dashboard outside its areas: it takes no \"area\".`);\n if (typeof entry.area !== 'string' || !isAreaName(entry.area.trim()))\n throw new Error(\n `${here} has an \"area\" that is not an area name: a lowercase word like \"trend\" (letters, digits and hyphens, at most 24).`,\n );\n if (entry.width !== undefined)\n throw new Error(`${here} has a \"width\" beside its \"area\": the area sets its width. Remove the \"width\".`);\n const section = sections[i];\n if (section) section.area = entry.area.trim();\n });\n });\n if (problems.length) throw new AppProblems(problems);\n return sections as AppDashSection[];\n}\n\n/**\n * A list view drawn inside a dashboard, read-only like any dashboard list:\n * the rows the view's room opens on (its standing query and a tab, or the\n * split's list query), drawn as its presentation reads — a grid or split as\n * a list of its show, a checklist or queue as a list of its card, a board as\n * a board. The dashboard's window and pick filters apply as to any section.\n */\nfunction embedSection(\n s: Record<string, unknown>,\n at: string,\n views: ReadonlyMap<string, ParsedView>,\n data: ParsedData,\n aside: boolean,\n): AppDashSection {\n allowKeys(s, VIEW_SECTION_KEYS, `${at} (a view)`);\n const listViews = [...views.values()].filter((view) => view.kind === 'list').map((view) => view.key);\n const parsed = typeof s.view === 'string' ? views.get(s.view) : undefined;\n if (!parsed || parsed.kind !== 'list') {\n throw new Error(\n `${at} embeds \"${String(s.view)}\", which is not a list view this App declares. Its list views: ${listViews.join(', ') || 'none'}.`,\n );\n }\n const { key, view } = parsed;\n const title = optionalText(s, 'title', at);\n const over = optionalText(s, 'over', at);\n if (s.width !== undefined && !WIDTHS.includes(String(s.width) as SectionWidth)) {\n throw new Error(`${at} has a \"width\" of \"${String(s.width)}\". Use ${WIDTHS.join(', ')}.`);\n }\n if (s.limit !== undefined && (typeof s.limit !== 'number' || !Number.isInteger(s.limit) || s.limit < 1)) {\n throw new Error(`${at} has a \"limit\" that is not a positive whole number.`);\n }\n let tab = view.tabs?.[0];\n if (s.tab !== undefined) {\n /* A tab is named by its address word, the one its URL carries (`?tab=`): a label is said in the reader's\n language and changes with `msg()`, the word does not. */\n tab = view.tabs?.find((candidate) => tabKey(candidate) === s.tab);\n if (!tab) {\n const byLabel = view.tabs?.find((candidate) => candidate.label === s.tab);\n throw new Error(\n byLabel\n ? `${at} names the tab \"${String(s.tab)}\" by its label. Name a tab by its address word: \"${tabKey(byLabel)}\".`\n : `${at} names the tab \"${String(s.tab)}\", which the view \"${key}\" does not have. ` +\n (view.tabs?.length ? `Its tabs: ${view.tabs.map((candidate) => tabKey(candidate)).join(', ')}.` : 'It has no tabs.'),\n );\n }\n }\n const presentation = view.presentation;\n const windows = parseSectionWindows(s, at);\n if (windows && presentation?.kind === 'board')\n throw new Error(`${at} embeds \"${key}\", a board, which takes the dashboard's window; its own \"windows\" are for a list.`);\n const interactive = embedInteractive(s, at, key, presentation?.kind, aside);\n /* What the embed says while it has nothing: its own guidance or 'hide', as a rows section takes; else the tab's,\n and on the first tab (the list as it opens) the list's own. A board says 'hide' or nothing: its lanes are its\n empty state. */\n if (s.empty === false) throw new Error(`${at} has \"empty: false\", which is now \"empty: 'hide'\".`);\n const own = s.empty === undefined || s.empty === 'hide' ? undefined : parseListEmpty(s.empty, at);\n if (own && presentation?.kind === 'board')\n throw new Error(`${at} embeds \"${key}\", a board, which takes \"empty: 'hide'\"; its lanes say it has nothing.`);\n const empty = own ?? tab?.empty ?? (tab === view.tabs?.[0] ? view.empty : undefined);\n const query = andQuery(view.query, tab?.query) || undefined;\n // The view's default order, where its tab says none of its own.\n const tabSorts = Boolean(tab?.query && /(^|\\s)(sort|order\\s+by)\\b/i.test(splitClauseTail(tab.query).tail));\n const base = {\n title: title ?? view.label ?? listLabel(view.table, data),\n table: view.table,\n ...(s.icon === undefined ? {} : { icon: parseIcon(s.icon, at) }),\n ...(query ? { query } : {}),\n ...(view.sort && !tabSorts ? { sort: view.sort } : {}),\n ...(s.width === undefined ? {} : { width: s.width as SectionWidth }),\n ...(s.limit === undefined ? {} : { limit: s.limit as number }),\n ...(over ? { over } : {}),\n ...(windows ? { windows } : {}),\n ...(interactive ? { interactive } : {}),\n ...(s.empty === 'hide' ? { emptyMode: 'hide' as const } : empty && presentation?.kind !== 'board' ? { empty } : {}),\n view: key,\n };\n if (!presentation || presentation.kind === 'split') {\n const show = view.show;\n const labels = labelsFor(show, data.resources?.[view.table]?.labels);\n return {\n kind: 'rows',\n ...base,\n ...(show ? { show } : {}),\n ...(labels ? { labels } : {}),\n ...(view.sizing === undefined ? {} : { sizing: view.sizing }),\n };\n }\n /* A custom list embedded is still its component's to draw (the rows section hands the view's presentation on). */\n if (presentation.kind === 'custom') return { kind: 'rows', ...base, show: [...presentation.show] };\n const card = [presentation.primary, ...('lines' in presentation ? (presentation.lines ?? []) : [])];\n if (presentation.kind === 'board') {\n if (s.width !== undefined) throw new Error(`${at} embeds \"${key}\", a board, which a dashboard draws full width. Remove \"width\".`);\n return {\n kind: 'board',\n ...base,\n by: presentation.by,\n ...(presentation.lanes ? { lanes: [...presentation.lanes] } : {}),\n show: card,\n };\n }\n return { kind: 'rows', ...base, show: card };\n}\n","/** What sdk/blocks says, in the custom components room (packages/kit/src/client/messages.ts says how the catalog is split). */\nimport { defineMessages } from '@mistriwork/kit/client/messages.js';\n\nexport const say = defineMessages({\n blockHasNo: '<{block}> has no \"{name}\". It takes {takes}.',\n blockRenamed: '<{block}> has \"{name}\", which is now \"{now}\".',\n blockNeeds: '<{block}> needs \"{name}\".',\n blockReadsUndeclared: '<{block}> reads \"{resource}\", which the page \"{label}\" does not read. It reads {reads}.',\n blockNotAResource: '<{block}> reads \"{resource}\", which is not one of this App\\'s resources.',\n blockValue: '<{block}> has a \"{name}\" of \"{value}\". Use {allowed}.',\n blockTabs: '<Tabs> needs \"tabs\": a list of tabs, each with its own \"key\", a \"label\" and its \"content\".',\n blockTabParam: '<Tabs> keeps its tab in \"{param}\", which is not one of the page\\'s params.',\n blockNoPage: 'This record has no page.',\n});\n","/**\n * The platform's blocks as a free-form page's components (`@mistriwork/mistri/page`): `<Rows>`, `<Metric>`, `<Chart>`,\n * `<Breakdown>`, `<RecordCard>`, `<Timeline>`, `<Tree>`, `<Gantt>`, `<Assistant>` and `<Tabs>`.\n *\n * Each is the component that draws the same thing on a dashboard, a record page or a list, handed the section it\n * would be handed there: no block draws anything of its own. Their props are the DSL's words for that section\n * (`metric('Open', 'ticket', { query, measure })` is `<Metric title=\"Open\" from=\"ticket\" query measure />`), so\n * there is one vocabulary, and each read goes through the same routes as the room's, under the reader's grants.\n *\n * This module is a part loaded when a page draws a block (page.tsx), and each block's own component is a part\n * loaded when that block is drawn: a page of metrics never fetches the Gantt.\n *\n * Each block's props are judged before it draws: a word it does not take, a value outside its words or a resource\n * the page does not read stands as the shared problem note in its place, saying which, marked as a part that could\n * not be drawn (an authoring mistake, which the browser suites fail on), while the rest of the page stays. What\n * throws while drawing stands behind the block's own boundary, as a dashboard section does.\n */\nimport { createElement, useContext, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';\nimport { pageFor, state } from '@mistriwork/kit/client/core.js';\nimport { tableNamed, type TableMeta } from '@mistriwork/engine/shared/schema.js';\nimport { parsePresentation } from '../../../shared/appViews.js';\nimport { BETTER, DASH_CHART_SHAPES } from '../../../shared/appModel.js';\nimport { FIGURE_FORMATS } from '@mistriwork/engine/shared/model.js';\nimport { TABLE_SIZINGS } from '@mistriwork/engine/shared/tableSizing.js';\nimport type { AppGanttList, AppTreeList } from '@mistriwork/engine/shared/listPresentations.js';\nimport { ErrorBoundary } from '@mistriwork/kit/client/components/ErrorBoundary.js';\nimport { Problem } from '@mistriwork/kit/client/components/Notes.js';\nimport { onDemand } from '@mistriwork/kit/client/components/onDemand.js';\nimport { captureRecordSurfaceOwner } from '@mistriwork/kit/client/components/recordSurface.js';\nimport type { Section } from '@mistriwork/kit/client/components/dashboardModel.js';\nimport type { PageSections } from '../RecordPageBody.js';\nimport type { PageEntry } from '@mistriwork/kit/client/components/pageModel.js';\nimport { UiPageContext, type UiPageFrame } from './context.js';\nimport { say } from '../../copy/components/components/sdk/blocks.js';\n\nconst DashboardBlock = onDemand(() => import('../Dashboard.js'), 'DashboardBlock');\nconst RecordPageBody = onDemand(() => import('../RecordPageBody.js'), 'RecordPageBody');\nconst TreeList = onDemand(() => import('../TreeList.js'), 'TreeList');\nconst GanttList = onDemand(() => import('../GanttList.js'), 'GanttList');\nconst AssistantRoom = onDemand(() => import('../AssistantPanel.js'), 'AssistantRoom');\nconst TabStrip = onDemand(() => import('@mistriwork/kit/client/components/PageSections.js'), 'TabStrip');\n\nconst WIDTHS = ['full', 'half', 'third', 'two-thirds'] as const;\nconst CURVES = ['smooth', 'straight'] as const;\nconst noop = () => {};\n\ntype Props = Readonly<Record<string, unknown>>;\n\n/* Words a block once took, refused with the one that replaced them. */\nconst RENAMED: Readonly<Record<string, string>> = { resource: 'from' };\n\n/* Each prop a block takes is one of its DSL words; anything else is a typo the author should hear about. */\nfunction takes(block: string, props: Props, words: readonly string[]): void {\n for (const name of Object.keys(props)) {\n if (words.includes(name)) continue;\n if (Object.hasOwn(RENAMED, name) && words.includes(RENAMED[name]))\n throw new Error(say('blockRenamed', { block, name, now: RENAMED[name] }));\n throw new Error(say('blockHasNo', { block, name, takes: words.join(', ') }));\n }\n}\n\nfunction needs(block: string, props: Props, name: string): string {\n const value = props[name];\n if (typeof value !== 'string' || !value.trim()) throw new Error(say('blockNeeds', { block, name }));\n return value.trim();\n}\n\nfunction oneOf<T extends string>(block: string, props: Props, name: string, allowed: readonly T[]): T | undefined {\n const value = props[name];\n if (value === undefined) return undefined;\n if (!allowed.includes(value as T)) {\n throw new Error(say('blockValue', { block, name, value: String(value), allowed: allowed.map((each) => `\"${each}\"`).join(', ') }));\n }\n return value as T;\n}\n\n/* The words a section carries as they are: text, a number, a list of text. The server's parser judges their sense. */\nfunction carried(props: Props, names: readonly string[]): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const name of names) if (props[name] !== undefined) out[name] = props[name];\n return out;\n}\n\n/** The table a block reads: one of the page's own reads, named as the App names it. */\nfunction resourceOf(block: string, resource: unknown, page: UiPageFrame | null): TableMeta {\n if (typeof resource !== 'string' || !resource) throw new Error(say('blockNeeds', { block, name: 'from' }));\n if (page && !page.reads.includes(resource)) {\n throw new Error(\n say('blockReadsUndeclared', { block, resource, label: page.label, reads: page.reads.map((name) => `\"${name}\"`).join(', ') }),\n );\n }\n const table = state.schema ? (tableNamed(state.schema, resource) as TableMeta | undefined) : undefined;\n if (!table) throw new Error(say('blockNotAResource', { block, resource }));\n return table;\n}\n\n/* A block: its props judged against the page it stands in, then drawn by the platform's component behind a boundary. */\nfunction block<R extends object>(judge: (props: Props, page: UiPageFrame | null) => R, draw: (resolved: R) => ReactNode) {\n return function Block(props: Props) {\n const page = useContext(UiPageContext);\n let resolved: R;\n try {\n resolved = judge(props, page);\n } catch (error) {\n const text = (error as Error).message;\n console.error(text);\n return <Problem crashed text={text} />;\n }\n return <ErrorBoundary>{createElement(draw as (props: R) => ReactNode, resolved)}</ErrorBoundary>;\n };\n}\n\n/* A dashboard's section, drawn where the page places it. Memoized on its words, so a page's redraw is not a refetch. */\nfunction SectionBlock({ section }: { section: Section }) {\n const words = JSON.stringify(section);\n const held = useMemo(() => section, [words]); // eslint-disable-line react-hooks/exhaustive-deps\n return <DashboardBlock section={held} />;\n}\n\nconst METRIC = ['title', 'from', 'query', 'measure', 'compare', 'better', 'format', 'unit', 'target', 'width'];\nfunction metricSection(props: Props, page: UiPageFrame | null): { section: Section } {\n takes('Metric', props, METRIC);\n const table = resourceOf('Metric', props.from, page);\n const title = needs('Metric', props, 'title');\n oneOf('Metric', props, 'better', BETTER);\n oneOf('Metric', props, 'format', FIGURE_FORMATS);\n oneOf('Metric', props, 'width', WIDTHS);\n /* A goal read from data is the metric's three words again; its table, where it names one, is one the page reads. */\n const goal = props.target;\n let target: Section['target'];\n if (goal !== null && typeof goal === 'object') {\n const asked = goal as { from?: string; table?: string; query?: string; measure?: string };\n if (asked.table !== undefined) throw new Error(say('blockRenamed', { block: 'Metric', name: 'target.table', now: 'target.from' }));\n target = {\n table: asked.from === undefined ? table.id : resourceOf('Metric', asked.from, page).id,\n ...(asked.query === undefined ? {} : { query: asked.query }),\n measure: asked.measure ?? (props.measure as string | undefined) ?? 'count',\n };\n } else if (goal !== undefined) target = goal as number;\n return {\n section: {\n kind: 'metric',\n table: table.id,\n ...carried(props, ['query', 'measure', 'compare', 'better', 'format', 'unit', 'width']),\n title,\n ...(target === undefined ? {} : { target }),\n } as Section,\n };\n}\n\n/* A chart's and a breakdown's figures are said as a metric's are: `format` and `unit` reach the dashboard's own sayer\n (dashboardModel's figureSayer), so a measure of seconds reads \"10h 2m\" on a bar, an axis and a tooltip. */\nconst CHART = [\n 'title',\n 'from',\n 'by',\n 'query',\n 'measure',\n 'shape',\n 'x',\n 'curve',\n 'total',\n 'target',\n 'limit',\n 'height',\n 'format',\n 'unit',\n 'width',\n];\nfunction chartSection(props: Props, page: UiPageFrame | null): { section: Section } {\n takes('Chart', props, CHART);\n const table = resourceOf('Chart', props.from, page);\n const title = needs('Chart', props, 'title');\n needs('Chart', props, 'by');\n oneOf('Chart', props, 'shape', DASH_CHART_SHAPES);\n oneOf('Chart', props, 'curve', CURVES);\n oneOf('Chart', props, 'format', FIGURE_FORMATS);\n oneOf('Chart', props, 'width', WIDTHS);\n return {\n section: {\n kind: 'chart',\n table: table.id,\n shape: 'line',\n ...carried(props, ['by', 'query', 'measure', 'shape', 'x', 'curve', 'total', 'target', 'limit', 'height', 'format', 'unit', 'width']),\n title,\n } as Section,\n };\n}\n\nconst BREAKDOWN = ['title', 'from', 'by', 'query', 'measure', 'limit', 'format', 'unit', 'width'];\nfunction breakdownSection(props: Props, page: UiPageFrame | null): { section: Section } {\n takes('Breakdown', props, BREAKDOWN);\n const table = resourceOf('Breakdown', props.from, page);\n const title = needs('Breakdown', props, 'title');\n needs('Breakdown', props, 'by');\n oneOf('Breakdown', props, 'format', FIGURE_FORMATS);\n oneOf('Breakdown', props, 'width', WIDTHS);\n return {\n section: {\n kind: 'breakdown',\n table: table.id,\n ...carried(props, ['by', 'query', 'measure', 'limit', 'format', 'unit', 'width']),\n title,\n } as Section,\n };\n}\n\nconst ROWS = ['title', 'from', 'query', 'show', 'sort', 'limit', 'sizing', 'width'];\nfunction rowsSection(props: Props, page: UiPageFrame | null): { section: Section } {\n takes('Rows', props, ROWS);\n const table = resourceOf('Rows', props.from, page);\n const title = needs('Rows', props, 'title');\n oneOf('Rows', props, 'sizing', TABLE_SIZINGS);\n oneOf('Rows', props, 'width', WIDTHS);\n const show = props.show as readonly string[] | undefined;\n return {\n section: {\n kind: 'rows',\n table: table.id,\n ...carried(props, ['query', 'sort', 'limit', 'sizing', 'width']),\n ...(show ? { show: [...show] } : {}),\n title,\n } as Section,\n };\n}\n\ninterface RecordAsk {\n table: TableMeta;\n recordKey: Record<string, unknown>;\n show?: readonly string[];\n only?: string;\n}\n\nfunction recordAsk(blockName: string, props: Props, page: UiPageFrame | null, only?: string): RecordAsk {\n const table = resourceOf(blockName, props.from, page);\n const record = props.record;\n if (!record || typeof record !== 'object' || Array.isArray(record))\n throw new Error(say('blockNeeds', { block: blockName, name: 'record' }));\n return {\n table,\n recordKey: record as Record<string, unknown>,\n ...(props.show === undefined ? {} : { show: props.show as readonly string[] }),\n ...(only ? { only } : {}),\n };\n}\n\n/* A record's own page, drawn in the page: its fields (or `show`'s) under its name and verbs, or only its timeline. */\nfunction RecordBlock({ table, recordKey, show, only }: RecordAsk) {\n const keyText = JSON.stringify(recordKey);\n const fields = show?.join('\\u0000');\n const mounted = useRef(true);\n useEffect(() => {\n mounted.current = true;\n return () => {\n mounted.current = false;\n };\n }, []);\n const owner = useMemo(() => captureRecordSurfaceOwner(() => mounted.current), [table.id, keyText]);\n const [pageId, setPageId] = useState<string | null>();\n useEffect(() => {\n let live = true;\n void (pageFor(table.id) as Promise<string | null>).then(\n (id) => live && setPageId(id ?? null),\n () => live && setPageId(null),\n );\n return () => {\n live = false;\n };\n }, [table.id]);\n const sections = useMemo(() => (show?.length ? ([{ kind: 'fields', paths: [...show] }] as PageSections) : undefined), [fields]); // eslint-disable-line react-hooks/exhaustive-deps\n if (pageId === null) return <p className=\"note\">{say('blockNoPage')}</p>;\n if (pageId === undefined) return null;\n return (\n <RecordPageBody\n key={keyText}\n owner={owner}\n tableId={table.id}\n recordKey={recordKey}\n pageId={pageId}\n close={noop}\n setPendingProbe={noop}\n onName={noop}\n onSettled={noop}\n {...(sections ? { sections } : {})}\n {...(only ? { only } : {})}\n />\n );\n}\n\n/* A tree and a Gantt chart are a list's presentations: their words are the presentation's, read by its own parser. */\ninterface Presented {\n table: TableMeta;\n spec: AppTreeList | AppGanttList;\n entry: { kind: 'table'; table: string; query: string };\n}\n\nfunction presented(blockName: string, kind: 'tree' | 'gantt', words: string[]) {\n return (props: Props, page: UiPageFrame | null): Presented => {\n takes(blockName, props, ['from', 'query', ...words]);\n const table = resourceOf(blockName, props.from, page);\n const spec = parsePresentation({ kind, ...carried(props, words) }, `<${blockName}>`) as AppTreeList | AppGanttList;\n return { table, spec, entry: { kind: 'table', table: table.id, query: (props.query as string | undefined) ?? '' } };\n };\n}\n\n/* Held on their words, so a page's redraw is not a refetch. */\nfunction TreeBlock({ table, spec, entry }: Presented) {\n const words = JSON.stringify([spec, entry]);\n const held = useMemo(() => ({ spec, entry }), [words]); // eslint-disable-line react-hooks/exhaustive-deps\n return <TreeList table={table} spec={held.spec as AppTreeList} entry={held.entry} />;\n}\n\nfunction GanttBlock({ table, spec, entry }: Presented) {\n const words = JSON.stringify([spec, entry]);\n const held = useMemo(() => ({ spec, entry }), [words]); // eslint-disable-line react-hooks/exhaustive-deps\n return <GanttList table={table} spec={held.spec as AppGanttList} entry={held.entry} />;\n}\n\ninterface TabSpec {\n key: string;\n label: ReactNode;\n content: ReactNode;\n}\n\ninterface TabsAsk {\n tabs: readonly TabSpec[];\n param?: string;\n label: string;\n page: UiPageFrame | null;\n}\n\nfunction tabsAsk(props: Props, page: UiPageFrame | null): TabsAsk {\n takes('Tabs', props, ['tabs', 'param', 'label']);\n const tabs = props.tabs as readonly TabSpec[] | undefined;\n if (\n !Array.isArray(tabs) ||\n !tabs.length ||\n tabs.some((tab) => !tab || typeof tab.key !== 'string' || !tab.key) ||\n new Set(tabs.map((tab) => tab.key)).size !== tabs.length\n ) {\n throw new Error(say('blockTabs'));\n }\n const param = props.param as string | undefined;\n if (param !== undefined && !page?.params.includes(param)) throw new Error(say('blockTabParam', { param: String(param) }));\n return { tabs, ...(param === undefined ? {} : { param }), label: typeof props.label === 'string' ? props.label : '', page };\n}\n\n/* Tabs, as a record page's are drawn: each panel drawn the first time it is opened, and kept in a page param if named. */\nfunction TabsBlock({ tabs, param, label, page }: TabsAsk) {\n const strip = tabs.map((tab) => ({ key: tab.key, token: tab.key, label: tab.label }));\n const entry = useMemo(() => ({ tab: param ? page?.get(param) : undefined }) as PageEntry, []); // eslint-disable-line react-hooks/exhaustive-deps\n return (\n <TabStrip\n tabs={strip}\n name={label}\n entry={entry}\n scoped={false}\n onTab={(token: string | undefined) => {\n if (param) page?.set(param, token);\n }}\n draw={(i: number) => tabs[i].content}\n />\n );\n}\n\nexport const Metric = block(metricSection, SectionBlock);\nexport const Chart = block(chartSection, SectionBlock);\nexport const Breakdown = block(breakdownSection, SectionBlock);\nexport const Rows = block(rowsSection, SectionBlock);\nexport const RecordCard = block((props, page) => {\n takes('RecordCard', props, ['from', 'record', 'show']);\n return recordAsk('RecordCard', props, page);\n}, RecordBlock);\nexport const Timeline = block((props, page) => {\n takes('Timeline', props, ['from', 'record']);\n return recordAsk('Timeline', props, page, 'timeline');\n}, RecordBlock);\nexport const Tree = block(presented('Tree', 'tree', ['parent', 'primary', 'lines']), TreeBlock);\nexport const Gantt = block(presented('Gantt', 'gantt', ['start', 'end', 'primary', 'by', 'progress']), GanttBlock);\nexport const Assistant = block(\n (props) => {\n takes('Assistant', props, ['conversation']);\n return typeof props.conversation === 'string' ? { conversation: props.conversation } : {};\n },\n (ask: { conversation?: string }) => <AssistantRoom {...ask} />,\n);\nexport const Tabs = block(tabsAsk, TabsBlock);\n"],"mappings":"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"}
1
+ {"version":3,"file":"shared-CL3RLKDA.js","names":["say","say","findTable","resourceOf"],"sources":["../../../engine/src/shared/tableSizing.ts","../../../engine/src/shared/formValues.ts","../../../engine/src/shared/appIcons.ts","../../../engine/src/shared/relatedCreate.ts","../../src/shared/appAddress.ts","../../src/shared/appModel.ts","../../src/shared/command.ts","../../../engine/src/shared/computedParse.ts","../../../engine/src/shared/funnel.ts","../../../engine/src/shared/pageLayouts.ts","../../../kit/src/client/recordCompose.ts","../../../kit/src/client/copy/record/components/recordSurface.ts","../../../kit/src/client/components/recordSurface.ts","../../../engine/src/shared/appComponents.ts","../../../engine/src/shared/pageConfig.ts","../../src/shared/formRules.ts","../../src/shared/appFlows.ts","../../src/shared/appDashSections.ts","../../src/shared/appViews.ts","../../src/client/copy/components/components/sdk/blocks.ts","../../src/client/components/sdk/blocks.tsx"],"sourcesContent":["export type TableSizing = 'fit' | 'scroll';\nexport const TABLE_SIZINGS: readonly TableSizing[] = ['fit', 'scroll'];\n\n/** Omission preserves the renderer's fit default without rewriting authored config. */\nexport function parseTableSizing(value: unknown, at: string): TableSizing | undefined {\n if (value === undefined) return undefined;\n if (value !== 'fit' && value !== 'scroll') throw new Error(`${at} has an invalid \"sizing\". Use \"fit\" or \"scroll\".`);\n return value;\n}\n","/**\n * How a form reads a record's own values, in the browser as someone types and on the server after: a condition in\n * the query language compiled to comparisons and judged against a row, a scalar a declaration may hold, the words a\n * default fills in (`today`, `now`, `me`), the starting values a schema's DEFAULTs already say, a stored value as an\n * expression reads it, and a multi-step form's step. The engine's, because the kit's New form draws a table's form\n * with them in both shells; the App framework's form declarations — their parsing, validation, child groups and\n * rules — are formRules.ts and formLines.ts, which re-export what they always named.\n */\nimport { columnKind, isFlagColumn, type ColumnMeta, type DatabaseSchema, type TableMeta } from './schema.js';\nimport type { ComputedValue } from './computed.js';\nimport { NO_ONE_HERE, parseQuery } from './query.js';\nimport type { Condition } from './filter.js';\n\n/** Data only: local columns, no paths, expressions, clock or executable hooks. */\nexport type FormScalar = string | number | boolean | null;\n/** The comparisons a compiled condition holds: the query language's own, named for the evaluator. */\nexport type FormOperator = 'eq' | 'ne' | 'lt' | 'lte' | 'gt' | 'gte';\n/**\n * A condition in the query language — the one a page section's `when` and an\n * action's `when` speak — read against a record's own values rather than run\n * as SQL: \"completed = false\", \"stage = closed_won and probability < 100\".\n * One language for every condition an App writes; what a form or a rule can\n * evaluate in memory is its comparisons of local columns (`=`, `!=`, `<`,\n * `<=`, `>`, `>=`, `in`, `is (not) empty`) under `and`, `or` and brackets.\n */\nexport type FormCondition = string;\n/** One compiled comparison: true when the field compares with any of the values (one, or an `in` list), or with\n * the record's own `other` column. */\ninterface Comparison {\n field: string;\n operator: FormOperator;\n values: readonly FormScalar[];\n other?: string;\n}\n/** A compiled condition: OR of ANDs, the query language's own filter shape. */\nexport type CompiledCondition = readonly (readonly Comparison[])[];\n\n/** A finite, data-only step in a multi-step form. Fields belong to one step. The\n * parser gives a step without an id its label's slug, as `step()` does. */\nexport interface FormStep<Field extends string = string> {\n id: string;\n /** The short name: the review's section, the stepper's entry and the progress bar's \"Step 2 of 5: Income\". */\n label: string;\n /** Every field the step asks, in order, rows read through. */\n fields: Field[];\n /** The fields as rows, where any row holds more than one (at most three side by side, stacking on a narrow\n screen); absent, one field to a row. Authored as an inner list: `[['first_name', 'last_name'], 'dob']`. */\n rows?: Field[][];\n /** The step's heading in the intake style (the room's h1); absent, its label. */\n title?: string;\n /** One sentence under the title. */\n lead?: string;\n /** The primary button's words on this step; absent, \"Continue\". */\n next?: string;\n /** The primary's words while every field of the step is empty (each optional): \"Skip\". */\n skip?: string;\n}\n\n/** How a multi-step form is drawn, both under one progress bar: `stepper` (the step's name over its fields) or `intake`\n (the step's title large, one question at a time, a sticky bar of actions and an editable review). */\nexport type FormStyle = 'stepper' | 'intake';\n\n/** Comparisons in one condition, counted after `or` is multiplied out. */\nexport const MAX_FORM_PREDICATES = 16;\n/** The longest condition text. */\nexport const MAX_FORM_CONDITION = 2000;\nconst MAX_FORM_TEXT = 4096;\n// eslint-disable-next-line no-control-regex -- Declaration names must exclude ASCII control characters.\nexport const LOCAL_FIELD = /^[^\\s.\\x00-\\x1f\\x7f]{1,256}$/u;\nexport const RESERVED = new Set(['__proto__', 'prototype', 'constructor']);\n\nfunction fail(where: string): never {\n // Never stringify rejected data: it may contain row values or executable hooks.\n throw new Error(`${where}: invalid form declaration.`);\n}\n\nexport function scalar(raw: unknown, where: string): FormScalar {\n if (raw === null) return null;\n if (typeof raw === 'boolean') return raw;\n if (typeof raw === 'string' && raw.length <= MAX_FORM_TEXT) return raw;\n if (typeof raw === 'number' && Number.isFinite(raw) && Math.abs(raw) <= Number.MAX_SAFE_INTEGER) return raw;\n return fail(where);\n}\n\nfunction frozen<T extends object>(value: T): T {\n return Object.freeze(value);\n}\n\nconst QUERY_OPERATORS: Readonly<Record<string, FormOperator>> = { '=': 'eq', '!=': 'ne', '<': 'lt', '<=': 'lte', '>': 'gt', '>=': 'gte' };\n\n/* The one table a condition is read against, for the parser: with the\n record's columns it types each literal by its column (\"02134\" stays text\n beside a zip code) and names a mistyped column; without, it judges the\n words alone, which is what an App's parse can know before a catalog. */\nfunction conditionSchema(name: string, columns: readonly ColumnMeta[]): DatabaseSchema {\n return {\n label: '',\n dialect: 'sqlite',\n readAt: '',\n foreignKeys: [],\n tables: [{ name, schema: '', id: name, isView: false, columns: [...columns] }],\n };\n}\n\n/* A validation reads its own columns and, through one reference, the record that reference names\n (`order_id.status`): the rule a child keeps about its parent (\"parts only while the order is on the bench\"),\n judged on the server with the parent read in the write's own transaction. One hop: a longer path is a rule\n about a record two tables away, which a command reading it states more plainly. */\nfunction readsThroughReference(column: string): boolean {\n const [via, name, ...more] = column.split('.');\n return !more.length && name !== undefined && [via, name].every((part) => LOCAL_FIELD.test(part) && !RESERVED.has(part));\n}\n\nfunction comparison(condition: Condition, where: string, paths: boolean): Comparison {\n if (!LOCAL_FIELD.test(condition.column) || RESERVED.has(condition.column)) {\n if (!paths || !readsThroughReference(condition.column))\n throw new Error(\n paths\n ? `${where}: \"${condition.column}\" goes past one reference; a validation reads the record's own columns and, through one reference, that record's (order_id.status).`\n : `${where}: \"${condition.column}\" is a path; a condition here reads the record's own columns.`,\n );\n }\n const values = (v: unknown): FormScalar[] => (Array.isArray(v) ? v : [v]).map((item) => scalar(item, where));\n if (condition.other !== undefined) {\n const op = QUERY_OPERATORS[condition.op];\n if (!op || !LOCAL_FIELD.test(condition.other) || RESERVED.has(condition.other)) fail(where);\n return { field: condition.column, operator: op, values: [], other: condition.other };\n }\n if (condition.op === 'isNull') return { field: condition.column, operator: 'eq', values: [null] };\n if (condition.op === 'isNotNull') return { field: condition.column, operator: 'ne', values: [null] };\n if (condition.op === 'in') return { field: condition.column, operator: 'eq', values: values(condition.value) };\n const op = QUERY_OPERATORS[condition.op];\n if (!op) {\n throw new Error(\n `${where}: \"${condition.op}\" matches text, which a condition here cannot; compare with =, !=, <, <=, >, >=, in or is empty.`,\n );\n }\n return { field: condition.column, operator: op, values: values(condition.value) };\n}\n\n/** A condition's text, compiled through the query language's own parser. `columns`, when known, type its literals. */\nexport function compileCondition(\n raw: unknown,\n where: string,\n columns?: readonly ColumnMeta[],\n table = 'record',\n paths = false,\n): CompiledCondition {\n if (typeof raw !== 'string' || !raw.trim() || raw.length > MAX_FORM_CONDITION) {\n throw new Error(`${where}: a condition is query-language text, like \"status = shipped\".`);\n }\n // Against the record's columns a bare word beside a number, date or flag may name another column.\n const parsed = parseQuery(`${table} ${raw}`, columns ? { schema: conditionSchema(table, columns), columnValues: true } : {});\n /* A form's condition and a validation are judged against the record's own values, in the browser as someone\n types and on the server after: no session is either place, so `me` is nobody's here (appMe.ts checkMe). */\n if (parsed.errors[0]?.message === NO_ONE_HERE)\n throw new Error(\n `${where} compares with me. A form's conditions and a validation read the record's own values, where the session is not; ` +\n 'show the field always, or narrow who may write it with roles.',\n );\n if (parsed.errors.length) throw new Error(`${where}: ${parsed.errors[0].message}`);\n const query = parsed.query!;\n if (query.orderBy.length || query.columns.length || query.limit !== undefined) {\n throw new Error(`${where}: a condition is only a condition — no sort, show or limit.`);\n }\n if (!query.filter.groups.length) throw new Error(`${where}: a condition is query-language text, like \"status = shipped\".`);\n const groups = query.filter.groups.map((group) => frozen(group.map((c) => frozen(comparison(c, where, paths)))));\n if (groups.flat().length > MAX_FORM_PREDICATES) throw new Error(`${where}: a condition compares at most ${MAX_FORM_PREDICATES} times.`);\n return frozen(groups);\n}\n\ntype Family = 'number' | 'boolean' | 'text' | 'date' | 'datetime' | 'instant';\nexport function family(column: ColumnMeta): Family | undefined {\n const type = column.type.trim().toLowerCase();\n if (/^(?:bool|boolean)$/.test(type)) return 'boolean';\n if (\n /^(?:(?:tiny|small|medium|big)?int(?:eger)?|int[248]|(?:small|big)?serial|numeric|decimal|real|double(?: precision)?|float[48]?)(?:\\s*\\(\\s*\\d+(?:\\s*,\\s*\\d+)?\\s*\\))?(?: unsigned)?$/.test(\n type,\n )\n )\n return 'number';\n if (/^(?:date)$/.test(type)) return 'date';\n if (/^(?:timestamptz(?:\\(\\d+\\))?|timestamp(?:\\(\\d+\\))? with time zone)$/.test(type)) return 'instant';\n if (/^(?:datetime|timestamp)(?:\\(\\d+\\))?(?: without time zone)?$/.test(type)) return 'datetime';\n if (\n column.allowed ||\n /^(?:text|(?:tiny|medium|long)text|n?varchar|(?:character varying)|n?char|character|uuid|clob)(?:\\(\\d+\\))?$/.test(type) ||\n type.startsWith('enum(')\n )\n return 'text';\n return undefined;\n}\n\ninterface Decimal {\n coefficient: bigint;\n scale: number;\n}\ntype Normalized =\n | { kind: 'null' }\n | { kind: 'number'; value: Decimal }\n | { kind: 'boolean'; value: boolean }\n | { kind: 'text' | 'date' | 'datetime' | 'instant'; value: string };\n\nfunction decimal(raw: unknown): Decimal | undefined {\n if (typeof raw === 'bigint') raw = raw.toString();\n if (typeof raw === 'number') {\n if (!Number.isFinite(raw) || Math.abs(raw) > Number.MAX_SAFE_INTEGER) return undefined;\n raw = String(raw);\n }\n if (typeof raw !== 'string' || raw.length > MAX_FORM_TEXT) return undefined;\n const match = /^([+-]?)(?:(\\d+)(?:\\.(\\d*))?|\\.(\\d+))(?:[eE]([+-]?\\d{1,4}))?$/.exec(raw.trim());\n if (!match) return undefined;\n const exponent = Number(match[5] ?? 0);\n if (Math.abs(exponent) > MAX_FORM_TEXT) return undefined;\n const fraction = match[3] ?? match[4] ?? '';\n let digits = ((match[2] ?? '0') + fraction).replace(/^0+/, '') || '0';\n let scale = fraction.length - exponent;\n while (digits.endsWith('0') && digits !== '0') {\n digits = digits.slice(0, -1);\n scale--;\n }\n return { coefficient: BigInt((match[1] === '-' ? '-' : '') + digits), scale: digits === '0' ? 0 : scale };\n}\n\nfunction validDate(value: string): boolean {\n const match = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(value);\n if (!match) return false;\n const [, y, m, d] = match;\n const year = Number(y),\n month = Number(m),\n day = Number(d);\n const days = [31, year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];\n return year >= 1 && month >= 1 && month <= 12 && day >= 1 && day <= days[month - 1];\n}\n\nfunction dateValue(raw: unknown, kind: 'date' | 'datetime' | 'instant'): string | undefined {\n // Date instances are accepted only for instants; interpreting local dates using\n // the machine's timezone would make validation depend on deployment settings.\n if (raw instanceof Date && kind === 'instant') {\n const time = Date.prototype.getTime.call(raw);\n if (!Number.isFinite(time)) return undefined;\n raw = new Date(time).toISOString();\n }\n if (typeof raw !== 'string' || raw.length > 64) return undefined;\n if (kind === 'date') return validDate(raw) ? raw : undefined;\n const match = /^(\\d{4}-\\d{2}-\\d{2})[T ](\\d{2}):(\\d{2})(?::(\\d{2})(?:\\.(\\d{1,9}))?)?(Z|[+-]\\d{2}(?::?\\d{2})?)?$/.exec(raw);\n if (!match || !validDate(match[1]) || Number(match[2]) > 23 || Number(match[3]) > 59 || Number(match[4] ?? 0) > 59) return undefined;\n const [, date, hour, minute, seconds = '00', fraction = '', zone] = match;\n if (kind === 'datetime') return zone ? undefined : `${date}T${hour}:${minute}:${seconds}.${fraction.padEnd(9, '0')}`;\n if (!zone) return undefined;\n let offset = 0;\n if (zone !== 'Z') {\n const parts = /^([+-])(\\d{2})(?::?(\\d{2}))?$/.exec(zone)!;\n const hours = Number(parts[2]),\n minutes = Number(parts[3] ?? 0);\n if (hours > 14 || minutes > 59 || (hours === 14 && minutes !== 0)) return undefined;\n offset = (hours * 60 + minutes) * (parts[1] === '-' ? -1 : 1);\n }\n const time = Date.parse(`${date}T${hour}:${minute}:${seconds}Z`) - offset * 60_000;\n // Store epoch nanoseconds as decimal text; comparison converts only this exact\n // internally produced integer, never the original DB identifier or timestamp.\n return String(BigInt(time) * 1_000_000n + BigInt(fraction.padEnd(9, '0')));\n}\n\nexport function normalize(column: ColumnMeta, raw: unknown): Normalized | undefined {\n const kind = family(column);\n if (!kind) return undefined;\n if (raw === null) return { kind: 'null' };\n if (kind === 'number') {\n const value = decimal(raw);\n if (!value || (/int|serial/i.test(column.type) && value.scale > 0)) return undefined;\n return { kind, value };\n }\n if (kind === 'boolean') {\n if (raw === true || raw === 1 || raw === 1n || (typeof raw === 'string' && /^(true|yes|1|t)$/i.test(raw.trim())))\n return { kind, value: true };\n if (raw === false || raw === 0 || raw === 0n || (typeof raw === 'string' && /^(false|no|0|f)$/i.test(raw.trim())))\n return { kind, value: false };\n return undefined;\n }\n if (kind === 'text') return typeof raw === 'string' && raw.length <= MAX_FORM_TEXT ? { kind, value: raw } : undefined;\n const value = dateValue(raw, kind);\n return value === undefined ? undefined : { kind, value };\n}\n\nfunction compare(left: Normalized | undefined, op: FormOperator, right: Normalized | undefined): boolean | 'unknown' {\n if (!left || !right) return 'unknown';\n if (left.kind === 'null' || right.kind === 'null') {\n if (op !== 'eq' && op !== 'ne') return 'unknown';\n const equal = left.kind === right.kind;\n return op === 'eq' ? equal : !equal;\n }\n if (left.kind !== right.kind) return 'unknown';\n let order: number;\n if (left.kind === 'number' && right.kind === 'number') {\n const scale = Math.max(left.value.scale, right.value.scale);\n const a = left.value.coefficient * 10n ** BigInt(scale - left.value.scale);\n const b = right.value.coefficient * 10n ** BigInt(scale - right.value.scale);\n order = a < b ? -1 : a > b ? 1 : 0;\n } else if (left.kind === 'instant' && right.kind === 'instant') {\n const a = BigInt(left.value),\n b = BigInt(right.value);\n order = a < b ? -1 : a > b ? 1 : 0;\n } else if (left.kind === 'boolean' && right.kind === 'boolean') {\n if (op !== 'eq' && op !== 'ne') return 'unknown';\n order = left.value === right.value ? 0 : 1;\n } else if (typeof left.value === 'string' && typeof right.value === 'string') {\n // Text ordering is deliberately unavailable: DB collations are not portable.\n if (left.kind === 'text' && op !== 'eq' && op !== 'ne') return 'unknown';\n order = left.value < right.value ? -1 : left.value > right.value ? 1 : 0;\n } else return 'unknown';\n switch (op) {\n case 'eq':\n return order === 0;\n case 'ne':\n return order !== 0;\n case 'lt':\n return order < 0;\n case 'lte':\n return order <= 0;\n case 'gt':\n return order > 0;\n case 'gte':\n return order >= 0;\n }\n}\n\nexport function columnNamed(columns: readonly ColumnMeta[], name: string): ColumnMeta | undefined {\n const matches = columns.filter((c) => c.name === name);\n return matches.length === 1 ? matches[0] : undefined;\n}\n\nexport function rowValue(row: Readonly<Record<string, unknown>>, name: string): unknown {\n const descriptor = Object.getOwnPropertyDescriptor(row, name);\n return descriptor && Object.hasOwn(descriptor, 'value') ? descriptor.value : undefined;\n}\n\nexport function comparisonResult(\n p: Comparison,\n row: Readonly<Record<string, unknown>>,\n columns: readonly ColumnMeta[],\n): boolean | 'unknown' {\n const column = columnNamed(columns, p.field);\n if (!column) return 'unknown';\n if (p.other !== undefined) {\n const other = columnNamed(columns, p.other);\n return !other || family(column) !== family(other)\n ? 'unknown'\n : compare(normalize(column, rowValue(row, p.field)), p.operator, normalize(other, rowValue(row, p.other)));\n }\n const results = p.values.map((value) => compare(normalize(column, rowValue(row, p.field)), p.operator, normalize(column, value)));\n return results.includes('unknown') ? 'unknown' : results.some((r) => r === true);\n}\n\nexport function conditionResult(\n condition: CompiledCondition,\n row: Readonly<Record<string, unknown>>,\n columns: readonly ColumnMeta[],\n): boolean | 'unknown' {\n const results = condition.map((group) => group.map((p) => comparisonResult(p, row, columns)));\n // Unknown dependencies never disappear behind a short-circuiting branch.\n if (results.flat().includes('unknown')) return 'unknown';\n return results.some((group) => group.every((r) => r === true));\n}\n\n/** Whether a record's values meet a condition: 'unknown' where a value cannot be compared, or the condition cannot be read against these columns. */\nexport function evaluateFormCondition(\n condition: FormCondition,\n row: Readonly<Record<string, unknown>>,\n columns: readonly ColumnMeta[],\n paths = false,\n): boolean | 'unknown' {\n let compiled: CompiledCondition;\n try {\n compiled = compileCondition(condition, 'Condition', columns, 'record', paths);\n } catch {\n return 'unknown';\n }\n return conditionResult(compiled, row, columns);\n}\n\n/** The word a form default fills in for its column — the constants `today`, `now`, `me` — or undefined for a literal. */\nexport function formDefaultWord(column: ColumnMeta, value: unknown): 'today' | 'now' | 'me' | undefined {\n // columnKind, not family: a SQLite TEXT declared a date (`TEXT as date`) is a date here.\n if (value === 'today' || value === 'now') return columnKind(column.type) === 'date' ? value : undefined;\n return value === 'me' && (column.references || columnKind(column.type) === 'text') ? 'me' : undefined;\n}\n\n/**\n * A New form's starting values the schema already says: a vocabulary column's\n * literal DEFAULT (`status … default 'booked'`) is the choice the form opens\n * on, so the person sees what the database would write. A clock or a sequence\n * stays the database's to fill. The form's own `defaults` win.\n */\nexport function inferredFormDefaults(table: { columns: readonly ColumnMeta[] }): Record<string, string> {\n const out: Record<string, string> = {};\n for (const column of table.columns) {\n if (!column.allowed?.length || column.flag || column.default === undefined) continue;\n const literal = /^'((?:[^']|'')*)'(?:::[\\w\\s.\"]+)?$/.exec(column.default.trim())?.[1]?.replace(/''/g, \"'\") ?? column.default.trim();\n if (column.allowed.includes(literal)) out[column.name] = literal;\n }\n return out;\n}\n\n/** A stored value as the evaluator reads it, by its column's kind: a number, a yes-or-no, a day, text; null for none. */\nexport function typedValue(table: TableMeta, column: string, value: unknown): ComputedValue {\n if (value === null || value === undefined || value === '') return null;\n const meta = table.columns.find((c) => c.name === column);\n if (!meta) return null;\n if (isFlagColumn(meta)) return value === true || value === 1 || /^(1|true|t|yes|y)$/i.test(String(value));\n const kind = columnKind(meta.type);\n if (kind === 'number') {\n const n = Number(value);\n return Number.isFinite(n) ? n : null;\n }\n if (kind === 'date') {\n const text = String(value);\n return /^\\d{4}-\\d{2}-\\d{2}$/.test(text) ? { day: text } : Number.isNaN(Date.parse(text)) ? null : { instant: new Date(text) };\n }\n return String(value);\n}\n","/**\n * The icons an App may name, and the check that an authored name is one.\n *\n * A menu entry's icon is a word in the manifest (`icon: 'kanban'`), never a\n * path or a component: a manifest is data, served to the browser and checked\n * by the server, and a word is what both sides can agree on. The words are\n * Phosphor's own names (https://phosphoricons.com, MIT, Phosphor Icons), so\n * an author can look one up there and see exactly what they will get; the\n * glyphs are drawn by `@phosphor-icons/react`, which the client already\n * depends on (packages/kit/src/client/components/Icon.tsx imports each one by itself).\n *\n * The set is curated rather than the whole pack of ~1,500: the icons business\n * Apps put in a menu — places, people, things, money, documents, time, state,\n * and a clinic's, a school's, a workshop's — and the marks a value or a change\n * wears (a display rule's `icons`, an activity feed's resources), and not the\n * glyphs neither has a use for. A glyph costs an App nothing it does not use:\n * the server sends the drawings of the icons an App names with its payload\n * (packages/mistri/src/server/app/iconPayload.ts), and the client bundles none of the set. So\n * the set grows by a word here and `node scripts/generate-icons.mjs`; the\n * tests keep this list, the JSON schema's `appIcon` enum, packages/mistri/schema/app.d.ts's\n * `AppIconName` and the generated glyphs the same.\n */\n\nexport const APP_ICONS = [\n // Places and structure\n 'house',\n 'tray',\n 'list',\n 'list-checks',\n 'kanban',\n 'table',\n 'calendar',\n 'calendar-check',\n 'clock',\n 'chart-bar',\n 'chart-line',\n 'chart-pie',\n 'gauge',\n 'target',\n 'flag',\n // People and organizations\n 'user',\n 'users',\n 'user-circle',\n 'address-book',\n 'handshake',\n 'buildings',\n 'storefront',\n 'briefcase',\n // Things and logistics\n 'package',\n 'truck',\n 'car',\n 'shopping-cart',\n // Money\n 'receipt',\n 'currency-dollar',\n 'credit-card',\n 'wallet',\n 'bank',\n // Documents\n 'file',\n 'file-text',\n 'folder',\n 'clipboard-text',\n 'note',\n 'archive',\n 'tag',\n 'ticket',\n 'star',\n 'bookmark-simple',\n // Communication\n 'bell',\n 'envelope',\n 'chat-circle',\n 'phone',\n 'headset',\n 'megaphone',\n 'globe',\n 'map-pin',\n // Tools and state\n 'magnifying-glass',\n 'gear',\n 'sliders',\n 'wrench',\n 'bug',\n 'shield-check',\n 'lock',\n 'key',\n 'database',\n 'stack',\n 'lightning',\n 'check-circle',\n 'warning',\n 'info',\n // Values: a priority's signal bars, a status's mark, what a change did\n 'cell-signal-low',\n 'cell-signal-medium',\n 'cell-signal-high',\n 'cell-signal-full',\n 'fire',\n 'hourglass',\n 'circle-dashed',\n 'x-circle',\n 'plus-circle',\n 'pencil-simple',\n 'paper-plane-tilt',\n 'arrows-left-right',\n 'arrow-circle-down',\n 'arrow-circle-up',\n // Devices: what a service catalog orders and an asset list holds\n 'laptop',\n 'monitor',\n 'device-mobile',\n 'printer',\n // Health and care: a clinic, a pharmacy, a lab\n 'stethoscope',\n 'heartbeat',\n 'heart',\n 'pill',\n 'syringe',\n 'first-aid-kit',\n 'thermometer',\n 'flask',\n 'test-tube',\n 'microscope',\n 'bed',\n 'hospital',\n 'ambulance',\n 'tooth',\n 'eyeglasses',\n 'wheelchair',\n 'baby',\n 'dna',\n 'brain',\n 'bandaids',\n 'hand-heart',\n // Learning, land and buildings\n 'graduation-cap',\n 'book-open',\n 'student',\n 'chalkboard-teacher',\n 'factory',\n 'warehouse',\n 'house-line',\n 'plant',\n 'leaf',\n 'sun',\n 'cloud',\n 'drop',\n // Travel\n 'airplane',\n 'bicycle',\n 'bus',\n 'train',\n 'boat',\n // Things, tools and records\n 'hammer',\n 'toolbox',\n 'plug',\n 'lightbulb',\n 'cube',\n 'camera',\n 'image',\n 'trophy',\n 'gift',\n 'coffee',\n 'fork-knife',\n 'shopping-bag',\n 'basket',\n 'barcode',\n 'qr-code',\n 'scales',\n 'gavel',\n 'certificate',\n 'identification-card',\n 'fingerprint',\n 'eye',\n 'link',\n 'paperclip',\n 'scissors',\n 'palette',\n 'code',\n 'terminal',\n 'users-three',\n 'user-plus',\n] as const;\n\nexport type AppIconName = (typeof APP_ICONS)[number];\n\nconst KNOWN: ReadonlySet<string> = new Set(APP_ICONS);\n\nexport const isAppIcon = (value: unknown): value is AppIconName => typeof value === 'string' && KNOWN.has(value);\n\n/* Edit distance, for \"did you mean\": a typo is a letter or two away, and a\n near synonym usually shares its first word (\"chart\" → chart-bar). */\nfunction distance(a: string, b: string): number {\n const row = Array.from({ length: b.length + 1 }, (_, i) => i);\n for (let i = 1; i <= a.length; i++) {\n let previous = row[0];\n row[0] = i;\n for (let j = 1; j <= b.length; j++) {\n const kept = row[j];\n row[j] = Math.min(row[j] + 1, row[j - 1] + 1, previous + (a[i - 1] === b[j - 1] ? 0 : 1));\n previous = kept;\n }\n }\n return row[b.length];\n}\n\nfunction nearest(name: string): string[] {\n const lower = name.toLowerCase();\n const stem = lower.split('-')[0];\n return APP_ICONS.map((icon) => ({ icon, d: icon.startsWith(stem) || icon.split('-').includes(stem) ? 0 : distance(lower, icon) }))\n .filter((each) => each.d <= 3)\n .sort((a, b) => a.d - b.d || a.icon.localeCompare(b.icon))\n .slice(0, 3)\n .map((each) => each.icon);\n}\n\n/** An authored `icon`: one of APP_ICONS, or refused with the closest names and where the rest are listed.\n * `key` is the key the author wrote it under — `ui.logo` takes an icon too, and a refusal naming \"icon\" sent people looking for a key they never wrote. */\nexport function parseIcon(value: unknown, at: string, key = 'icon'): AppIconName {\n if (isAppIcon(value)) return value;\n if (typeof value !== 'string' || !value.trim())\n throw new Error(`${at} has ${/^[aeiou]/.test(key) ? 'an' : 'a'} \"${key}\" that is not an icon's name.`);\n const near = nearest(value.trim());\n throw new Error(\n `${at} has \"${key}\": \"${value}\", which is not one of the App icons.` +\n `${near.length ? ` Did you mean ${near.map((each) => `\"${each}\"`).join(', ')}?` : ''} The icons: ${APP_ICONS.join(', ')}.`,\n );\n}\n","import { authoredValue } from './autoValues.js';\nimport { autoKey, isRequired } from './form.js';\nimport { findTable, primaryKey, type DatabaseSchema, type TableMeta } from './schema.js';\nimport { hopName } from './view.js';\nimport { parseQuery } from './query.js';\nimport type { RelatedCreate } from '../../schema/model.js';\n\ntype Scalar = string | number | boolean | null;\nconst CREATE_KEYS = ['label', 'show', 'defaults', 'prefill'];\n/** Columns filled from the page's record, at most. */\nconst MAX_FILL = 8;\nconst COLUMN = /^[A-Za-z_][A-Za-z0-9_]*$/;\n\n// A tenant host hands its pages over as captured, null-prototype copies (evidence.ts), and those are plain too.\nconst isPlain = (value: unknown): value is Record<string, unknown> =>\n !!value && typeof value === 'object' && !Array.isArray(value) && [null, Object.prototype].includes(Object.getPrototypeOf(value));\n\nexport function parseRelatedCreate(raw: unknown, at: string): RelatedCreate<string> {\n if (typeof raw === 'string') raw = { label: raw };\n if (!isPlain(raw)) throw new Error(`${at}: create must be an object.`);\n const value = raw;\n if (Object.keys(value).some((key) => !CREATE_KEYS.includes(key)))\n throw new Error(`${at}: create accepts label, show, defaults and prefill.`);\n if (typeof value.label !== 'string' || !value.label.trim()) throw new Error(`${at}: create needs a label.`);\n if (value.show !== undefined && (!Array.isArray(value.show) || value.show.some((c) => typeof c !== 'string' || !c.trim())))\n throw new Error(`${at}: create.show must contain column names.`);\n let defaults: Record<string, Scalar> | undefined;\n if (value.defaults !== undefined) {\n if (!isPlain(value.defaults)) throw new Error(`${at}: create.defaults is an object of column → value.`);\n defaults = {};\n for (const [column, authored] of Object.entries(value.defaults)) {\n if (!COLUMN.test(column)) throw new Error(`${at}: create.defaults \"${column}\" is not a column name.`);\n const v = authoredValue(authored, `${at}: create.defaults \"${column}\"`);\n if (!(v === null || typeof v === 'string' || typeof v === 'boolean' || (typeof v === 'number' && Number.isFinite(v)))) {\n throw new Error(`${at}: create.defaults \"${column}\" is a text, number, true/false, null, today, now or me.`);\n }\n defaults[column] = v as Scalar;\n }\n }\n let prefill: Record<string, string> | undefined;\n if (value.prefill !== undefined) {\n const pairs = isPlain(value.prefill) ? Object.entries(value.prefill) : [];\n if (\n !pairs.length ||\n pairs.length > MAX_FILL ||\n pairs.some(([child, parent]) => !COLUMN.test(child) || typeof parent !== 'string' || !COLUMN.test(parent))\n ) {\n throw new Error(\n `${at}: create.prefill maps one to ${MAX_FILL} of the new row's columns to the record's: { patient_id: 'patient_id' }.`,\n );\n }\n prefill = Object.fromEntries(pairs) as Record<string, string>;\n }\n return {\n label: value.label.trim(),\n ...(value.show === undefined ? {} : { show: (value.show as string[]).map((c) => c.trim()) }),\n ...(defaults ? { defaults } : {}),\n ...(prefill ? { prefill } : {}),\n };\n}\n\n/** Whether a create says more than its label, so it cannot be written as the label alone. */\nexport function createSaysMore(create: RelatedCreate<string>): boolean {\n return create.show !== undefined || create.defaults !== undefined || create.prefill !== undefined;\n}\n\n/**\n * What a filtered related list's query pins: its equalities (`status = open`,\n * `kind in (visit)`), read by the query language's own parser over the child\n * table. A row added from the list starts with them, so it is listed where it\n * was added. An `or`, a range or a path pins nothing.\n */\nfunction filterEqualities(schema: DatabaseSchema, from: TableMeta, filter: string | undefined): Record<string, Scalar> {\n if (!filter?.trim()) return {};\n const parsed = parseQuery(`${from.id} ${filter}`, { schema });\n const groups = parsed.query?.filter.groups;\n if (parsed.errors.length || !groups || groups.length !== 1) return {};\n const pinned: Record<string, Scalar> = {};\n for (const condition of groups[0]) {\n if (condition.column.includes('.') || !from.columns.some((c) => c.name === condition.column)) continue;\n const value =\n condition.op === '='\n ? condition.value\n : condition.op === 'in' && Array.isArray(condition.value) && condition.value.length === 1\n ? condition.value[0]\n : undefined;\n if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') pinned[condition.column] = value;\n }\n return pinned;\n}\n\n/** Bind only a direct FK to the whole parent PK, never inferred query equalities.\n * `filter` is the related list's own query; the values it pins fill the new row\n * as defaults do, never as bindings. */\nexport function relatedCreatePlan(\n schema: DatabaseSchema,\n baseId: string,\n fromId: string,\n path: string,\n create: RelatedCreate<string>,\n list: { filter?: string } = {},\n) {\n const base = findTable(schema, baseId);\n const from = findTable(schema, fromId);\n const candidates = schema.foreignKeys.filter((fk) => fk.from.table === fromId);\n const fk = candidates.find((fk) => fk.name === path) ?? candidates.find((fk) => hopName(fk) === path);\n /* A wrong `whose` is the usual mistake, so it is named for what it is —\n not a column, or a column that points elsewhere — with the references\n that would do, rather than as a rule about primary keys. */\n if (base && from && !fk && !path.includes('.')) {\n const toBase = candidates.filter((ref) => ref.to.table === baseId).map((ref) => `\"${hopName(ref)}\"`);\n const could = toBase.length\n ? ` Its references to ${base.name}: ${toBase.join(', ')}.`\n : ` ${from.name} has no reference to ${base.name}.`;\n return {\n error: from.columns.some((c) => c.name === path)\n ? `\"${path}\" is a column of ${from.name}, but not a reference to ${base.name}.${could}`\n : `\"${path}\" is not a column of ${from.name}.${could}`,\n };\n }\n if (base && fk && fk.to.table !== baseId) {\n return {\n error: `\"${path}\" refers to ${findTable(schema, fk.to.table)?.name ?? fk.to.table}, not to ${base.name}, so a row added here would not belong to this record.`,\n };\n }\n if (\n !base ||\n !from ||\n from.isView ||\n !fk ||\n fk.to.columns.length !== primaryKey(base).length ||\n !primaryKey(base).every((c) => fk.to.columns.includes(c))\n ) {\n return { error: 'Related creation needs a direct reference to the complete parent primary key.' };\n }\n const show = create.show ?? from.columns.map((c) => c.name);\n if (show.some((name) => !from.columns.some((c) => c.name === name))) return { error: 'Related creation names an unknown form column.' };\n const unknownDefault = Object.keys(create.defaults ?? {}).find((name) => !from.columns.some((c) => c.name === name));\n if (unknownDefault) return { error: `Related creation defaults \"${unknownDefault}\", which is not a column of ${from.name}.` };\n /* A filled column comes from the record: the child's column, the record's column. */\n const fill: Record<string, string> = { ...(create.prefill ?? {}) };\n for (const [child, parent] of Object.entries(fill)) {\n if (!from.columns.some((c) => c.name === child))\n return { error: `Related creation fills \"${child}\", which is not a column of ${from.name}.` };\n if (!base.columns.some((c) => c.name === parent))\n return { error: `Related creation fills \"${child}\" from \"${parent}\", which is not a column of ${base.name}.` };\n if (fk.from.columns.includes(child))\n return { error: `Related creation fills \"${child}\", the reference to ${base.name} it already binds.` };\n }\n const pinned = filterEqualities(schema, from, list.filter);\n const given = new Set([...Object.keys(create.defaults ?? {}), ...Object.keys(fill), ...Object.keys(pinned)]);\n const assigned = autoKey(from);\n const hiddenRequired = from.columns.filter(\n (c) => isRequired(c, assigned) && !show.includes(c.name) && !fk.from.columns.includes(c.name) && !given.has(c.name),\n );\n if (hiddenRequired.length)\n return { error: `Related creation must show required columns: ${hiddenRequired.map((c) => c.name).join(', ')}.` };\n /* A required reference to another record (a work order's bike, added from its mechanic): the record cannot say it,\n so a New the list offers on its own would ask a whole other choice; one the author writes still may. */\n const otherParents = schema.foreignKeys\n .filter((ref) => ref !== fk && ref.from.table === fromId)\n .flatMap((ref) => ref.from.columns)\n .filter((name, index, all) => all.indexOf(name) === index && !fk.from.columns.includes(name) && !given.has(name))\n .filter((name) => {\n const column = from.columns.find((c) => c.name === name);\n return column !== undefined && isRequired(column, assigned);\n });\n return {\n show: show.filter((c) => !fk.from.columns.includes(c)),\n bindings: fk.from.columns.map((column, i) => ({ column, parent: fk.to.columns[i] })),\n otherParents,\n // The list's own equalities win: a row added here is listed here.\n defaults: { ...(create.defaults ?? {}), ...pinned },\n fill,\n };\n}\n","/**\n * An App's addresses: one canonical URI per thing it shows.\n *\n * A record is `/{app}/{route}/{id}` — `/compass/leads/127` — where the route\n * is the view that shows its table, so following a reference from a lead to\n * its account opens `/compass/accounts/13`, the account's own address, not a\n * path that grows with every hop. The walk that got there rides in history\n * state (client url.js), so Back and the crumbs still know it, and a link\n * pasted into chat is the record alone.\n *\n * The route a table's records are addressed through is decided once, here,\n * the same way for the client, the server and `mistri check`, over the\n * views the person reading is served — so a resident's repair is\n * `/my-requests/12` and the office's the same record at `/repairs/12`:\n *\n * 1. the resource's `view`, when it names one of the list views over it\n * that this reader is served — a preference, passed over quietly for a\n * person whose role is not served that view;\n * 2. else the first list view over the table in the menu's order that has\n * no standing query and is not a split — the plain list of them — then\n * the first without a standing query, then the first;\n * 3. else the page its records open through, by its `path` or key;\n * 4. else the table's own name.\n *\n * A view's route is its `path`, or its key; a page's likewise. Dashboards,\n * free-form pages and forms are their route alone. What the room on screen is showing\n * rides the address's parameters (client publicAddress.ts writes and reads\n * them; `mistri check --routes` lists each one a route reads):\n *\n * list `tab=` (a tab's label as a word; none is the first), `search=`\n * (the name column's box), one per filter column\n * (`make=Toyota`, `make=Toyota,Honda`, `price=..10000`,\n * `year=2018..`), `sort=-price,year`, and `q=` for a question\n * those cannot say, whole\n * dashboard `period=` (one of its windows, as a word; `all-time` where it\n * opens on a `window` of its own), one per pick column\n * (`owner_id=12`), and `period.<section>=` for a section with\n * windows of its own, the section named by its title\n * free-form the parameters its `definePage` declares (`params: ['list',\n * page 'line']` → `?list=12&line=3`), and nothing else\n *\n * A parameter a route does not offer is not read: a link naming a window or\n * a column the view no longer has lands on the view as it opens.\n *\n * The id segment is the primary key's value, or — where the resource says\n * `address: '<column>'` — that column's (a natural key the check proves NOT\n * NULL and unique). Composite keys are said under their parent where one of\n * their two columns is a reference to it (`/orders/12/lines/3`), otherwise\n * by name (`order_id=12,line=3`). A slug (`slug: true`) follows a numeric id\n * on a public page — `/forecourt/vehicles/1-2021-toyota-corolla` — and only\n * the leading id is read back, so any spelling of the rest finds the record.\n *\n * Values stay text end to end: an id past 2^53 is a string from the adapter\n * and a string here, never run through Number().\n *\n * Pure and dependency-light on purpose: the server's canonical redirect,\n * the check's route table and the browser's address bar must never disagree\n * about what an address means.\n */\nimport { filterAddressWord } from '@mistriwork/engine/shared/listFilters.js';\nimport { slug as slugOf } from '@mistriwork/engine/shared/slug.js';\nimport { tabKey } from '@mistriwork/engine/shared/addressParams.js';\nimport { SERVER_SEGMENTS } from '@mistriwork/engine/shared/mount.js';\nimport { ASSISTANT_ROOM } from '@mistriwork/engine/shared/servedApp.js';\nexport { ASSISTANT_ROOM } from '@mistriwork/engine/shared/servedApp.js';\n\n/** Words an App route may not take: `new` is the New form's, the rest the server's own (mount.ts). */\nexport const RESERVED_PATHS: readonly string[] = ['new', ...SERVER_SEGMENTS, '_components', 'sign-in', 'welcome', 'form'];\n\n/** The signed-in person's notifications, in an App that keeps an inbox (appNotify.ts). */\nexport const INBOX_SEGMENT = 'notifications';\n\n/** One route segment as an author spells it in `path:`: the letters a view key allows. */\nconst ROUTE_PATH = /^[a-z0-9][a-z0-9-]{0,63}$/;\n\ninterface ColumnLike {\n name: string;\n primaryKey: boolean;\n references?: { table: string; column: string };\n}\ninterface TableLike {\n id: string;\n name: string;\n columns: readonly ColumnLike[];\n}\ninterface ViewLike {\n table?: string;\n path?: string;\n query?: string;\n presentation?: { kind?: string };\n sections?: unknown;\n command?: unknown;\n}\n\nexport interface AddressBookInput {\n /** The App's views by key: a list names its `table`, a dashboard has `sections`, a form a `command`. */\n views: Readonly<Record<string, ViewLike>>;\n /** View keys in the menu's order (viewOrder): which list is a table's first. */\n order: readonly string[];\n /** What each resource says about its addresses, by resolved table id. */\n resources: Readonly<Record<string, { view?: string; address?: string; slug?: boolean }>>;\n /** The App's pages in load order — the first over a base is the one its records open through. */\n pages: readonly { id: string; base: string; path?: string; document?: unknown }[];\n tables: readonly TableLike[];\n /** The App keeps an inbox, whose room is `/{app}/notifications`. */\n inbox?: boolean;\n /** The assistant has a page of its own, `/{app}/assistant`, for the person served (appPayload.ts). */\n assistant?: boolean;\n /** The views are what one person is served, not the whole App's: a resource's `view` naming one they are not\n * served is that person's to pass over, not a problem (the whole App's book still says a misnamed one). */\n projected?: boolean;\n}\n\n/** What a route's segment names. */\nexport type RouteTarget =\n | {\n kind: 'list';\n view: string;\n table: string;\n /** The words of the tabs after the first, which is the list as it opens (`?tab=decided`). */\n tabs?: string[];\n /** The filter columns it is refined by (`?make=Toyota`). */\n filters?: string[];\n }\n | {\n kind: 'dashboard';\n view: string;\n /** The dashboard offers windows of its own (`?period=`). */\n window?: true;\n /** The columns its picks are said by (`?owner_id=12`). */\n picks?: string[];\n /** Sections with windows of their own, by the name each rides the address under (`?period.<name>=`). */\n periods?: string[];\n /** A free-form page (`definePage`), drawn as a dashboard of its one component. */\n freeForm?: true;\n /** A free-form page's parameters it keeps its place in (`?list=12&line=3`). */\n params?: string[];\n }\n | { kind: 'form'; view: string }\n | { kind: 'page'; page: string; table: string }\n | { kind: 'table'; table: string }\n | { kind: 'inbox' }\n | { kind: 'assistant' };\n\n/** How one table's records are addressed. */\ninterface RecordRoute {\n segment: string;\n table: string;\n target: RouteTarget;\n /** The natural key column (`address:`), when the resource names one. */\n column?: string;\n /** A cosmetic slug follows a numeric id on public pages. */\n slug?: true;\n /** A two-column key one of whose columns references its parent: said under the parent's address. */\n parent?: { column: string; table: string; child: string };\n}\n\nexport interface AddressBook {\n /** Segment → what it names. */\n routes: Map<string, RouteTarget>;\n /** View key → its segment. */\n views: Map<string, string>;\n /** Page id → its segment, where it has one of its own. */\n pages: Map<string, string>;\n /** Page id → its base table. */\n pageBase: Map<string, string>;\n /** Table id → the page its records open through. */\n defaultPage: Map<string, string>;\n /** Table id → how its records are addressed. */\n records: Map<string, RecordRoute>;\n tables: Map<string, TableLike>;\n /** Collisions and misnamed routes, for the check and the shell's count. */\n problems: string[];\n}\n\n/** An authored `path:` — one route segment, not a reserved word — or a refusal that names the fix. */\nexport function parseRoutePath(value: unknown, at: string): string | undefined {\n if (value === undefined) return undefined;\n if (typeof value !== 'string' || !ROUTE_PATH.test(value)) {\n throw new Error(`${at}: \"path\" must be one address segment — lowercase letters, digits and hyphens, like \"sales-dashboard\".`);\n }\n if (RESERVED_PATHS.includes(value)) throw new Error(`${at}: \"path\" cannot be \"${value}\"; ${RESERVED_PATHS.join(', ')} are reserved.`);\n return value;\n}\n\nconst isDashboard = (view: ViewLike) => Object.hasOwn(view, 'sections');\nconst words = (value: unknown): string[] => (Array.isArray(value) ? value.filter((word): word is string => typeof word === 'string') : []);\n/* What a dashboard's address may say beyond its route: its window, its picks,\n each windowed section's own window — or, for a free-form page, the place it keeps. */\nfunction parametersOf(\n view: ViewLike & { aside?: unknown; windows?: unknown; filters?: unknown; page?: { params?: unknown } },\n): Omit<Extract<RouteTarget, { kind: 'dashboard' }>, 'kind' | 'view'> {\n if (view.page) {\n const params = words(view.page.params);\n return { freeForm: true as const, ...(params.length ? { params } : {}) };\n }\n const periods = [view.sections, view.aside]\n .flatMap((list) => (Array.isArray(list) ? list : []))\n .flatMap((section: { windowKey?: unknown }) => (typeof section?.windowKey === 'string' ? [section.windowKey] : []));\n const picks = words(view.filters);\n return {\n ...(words(view.windows).length ? { window: true as const } : {}),\n ...(picks.length ? { picks } : {}),\n ...(periods.length ? { periods } : {}),\n };\n}\n/** A list's parameters as the route table says them: its tabs by their words, and its filter columns. */\nfunction listParametersOf(\n view: ViewLike & { tabs?: unknown; filters?: unknown },\n): Omit<Extract<RouteTarget, { kind: 'list' }>, 'kind' | 'view' | 'table'> {\n const tabs = (Array.isArray(view.tabs) ? view.tabs : [])\n .slice(1)\n .flatMap((tab: { label?: unknown; slug?: string }) =>\n typeof tab?.label === 'string' && tabKey({ label: tab.label, slug: tab.slug }) ? [tabKey({ label: tab.label, slug: tab.slug })] : [],\n );\n // Each by its address word: a `has` filter's is `has.<table>.<column>` (listFilters.ts).\n const filters = words(view.filters).map(filterAddressWord);\n return { ...(tabs.length ? { tabs } : {}), ...(filters.length ? { filters } : {}) };\n}\nconst isForm = (view: ViewLike) => Object.hasOwn(view, 'command');\nconst isList = (view: ViewLike | undefined): view is ViewLike & { table: string } =>\n Boolean(view && !isDashboard(view) && !isForm(view) && typeof view.table === 'string');\n\nconst describe = (target: RouteTarget): string => {\n switch (target.kind) {\n case 'list':\n return `the list view \"${target.view}\"`;\n case 'dashboard':\n return target.freeForm ? `the free-form page \"${target.view}\"` : `the dashboard \"${target.view}\"`;\n case 'form':\n return `the form \"${target.view}\"`;\n case 'page':\n return `the page \"${target.page}\"`;\n case 'inbox':\n return 'the notifications inbox';\n case 'assistant':\n return \"the assistant's page\";\n default:\n return `the table \"${target.table}\"`;\n }\n};\n\nconst primaryKeyOf = (table: TableLike | undefined): string[] =>\n table ? table.columns.filter((c) => c.primaryKey).map((c) => c.name) : [];\n\n/** Every route the App answers to, and how each table's records are addressed. */\nexport function appAddressBook(input: AddressBookInput): AddressBook {\n const book: AddressBook = {\n routes: new Map(),\n views: new Map(),\n pages: new Map(),\n pageBase: new Map(),\n defaultPage: new Map(),\n records: new Map(),\n tables: new Map(input.tables.map((table) => [table.id, table])),\n problems: [],\n };\n const reserved = new Set(RESERVED_PATHS);\n /* Claimed in a fixed order — views in the menu's, then pages, then the\n tables nothing else shows — so which of two claimants a collision keeps\n never depends on object key order. */\n const claim = (segment: string, target: RouteTarget, what: string, said: boolean): boolean => {\n if (reserved.has(segment)) {\n if (said) book.problems.push(`${what}: its address \"${segment}\" is reserved (${RESERVED_PATHS.join(', ')}). Give it another \"path\".`);\n return false;\n }\n\n const held = book.routes.get(segment);\n if (held) {\n if (said)\n book.problems.push(\n `${what}: its address \"/${segment}\" is already ${describe(held)}'s. Two routes cannot share a path; give one of them another \"path\".`,\n );\n return false;\n }\n book.routes.set(segment, target);\n return true;\n };\n /* Claimed first: an App that keeps an inbox has it at one address in\n every App, and a view that would take the word is told to take another. */\n if (input.inbox) book.routes.set(INBOX_SEGMENT, { kind: 'inbox' });\n if (input.assistant) book.routes.set(ASSISTANT_ROOM, { kind: 'assistant' });\n\n for (const key of input.order) {\n const view = input.views[key];\n if (!view) continue;\n const target: RouteTarget | undefined = isDashboard(view)\n ? { kind: 'dashboard', view: key, ...parametersOf(view) }\n : isForm(view)\n ? { kind: 'form', view: key }\n : isList(view)\n ? { kind: 'list', view: key, table: view.table, ...listParametersOf(view) }\n : undefined;\n if (!target) continue;\n const segment = view.path ?? key;\n // A key is always said: a view is its address, and a collision there is the author's to resolve.\n if (claim(segment, target, `views \"${key}\"`, true)) book.views.set(key, segment);\n }\n const listed = new Set([...book.views.keys()].flatMap((key) => (isList(input.views[key]) ? [input.views[key].table] : [])));\n for (const page of input.pages) {\n /* A document is said under its record (`/orders/12/invoice`, appDocuments.ts): no route, and never the page a\n record opens through. */\n if (page.document !== undefined) continue;\n book.pageBase.set(page.id, page.base);\n const first = !book.defaultPage.has(page.base);\n if (first) book.defaultPage.set(page.base, page.id);\n const segment = page.path ?? page.id;\n /* A page answers to its own address only where it needs one: its `path`\n is written, its base has no list to be addressed through, or it is a\n second page over the base. Otherwise `/compass/lead` stays what it\n always was — the lead table, read the older way — and the lead page's\n records are `/compass/leads/127`. A key a view already answers to is\n no collision the author made; a `path` they wrote is theirs, and said. */\n if (page.path === undefined && (!ROUTE_PATH.test(segment) || (first && listed.has(page.base)))) continue;\n if (claim(segment, { kind: 'page', page: page.id, table: page.base }, `pages \"${page.id}\"`, page.path !== undefined))\n book.pages.set(page.id, segment);\n }\n\n for (const table of input.tables) {\n const resource = Object.hasOwn(input.resources, table.id) ? input.resources[table.id] : undefined;\n const lists = input.order.filter((key) => book.views.has(key) && isList(input.views[key]) && input.views[key].table === table.id);\n let view: string | undefined;\n if (resource?.view !== undefined) {\n if (lists.includes(resource.view)) view = resource.view;\n else if (input.projected && !Object.hasOwn(input.views, resource.view)) view = undefined;\n else\n book.problems.push(\n `resources \"${table.name}\": \"view\" names \"${resource.view}\", which is not a list view over \"${table.name}\". It names the view its records are addressed through.`,\n );\n }\n view ??=\n lists.find((key) => !input.views[key].query && input.views[key].presentation?.kind !== 'split') ??\n lists.find((key) => !input.views[key].query) ??\n lists[0];\n let route: RecordRoute | undefined;\n if (view) route = { segment: book.views.get(view)!, table: table.id, target: { kind: 'list', view, table: table.id } };\n const page = book.defaultPage.get(table.id);\n if (!route && page && book.pages.has(page))\n route = { segment: book.pages.get(page)!, table: table.id, target: { kind: 'page', page, table: table.id } };\n if (!route) {\n const target: RouteTarget = { kind: 'table', table: table.id };\n // Only a resource the App declares is the author's to be told about.\n if (claim(table.id, target, `resources \"${table.name}\"`, Boolean(resource))) route = { segment: table.id, table: table.id, target };\n }\n if (!route) continue;\n if (resource?.address !== undefined) {\n if (table.columns.some((column) => column.name === resource.address)) route.column = resource.address;\n else\n book.problems.push(`resources \"${table.name}\": \"address\" names \"${resource.address}\", which is not a column of \"${table.name}\".`);\n }\n if (resource?.slug) route.slug = true;\n book.records.set(table.id, route);\n }\n\n /* Second pass: a parent must itself have an address said by its primary\n key, or the child's could not be written before the parent's name is\n known. */\n for (const route of book.records.values()) {\n if (route.column) continue;\n const key = book.tables.get(route.table)!.columns.filter((column) => column.primaryKey);\n if (key.length !== 2) continue;\n for (const [i, column] of key.entries()) {\n const reference = column.references;\n if (!reference || reference.table === route.table) continue;\n const parent = book.records.get(reference.table);\n const parentKey = primaryKeyOf(book.tables.get(reference.table));\n if (!parent || parent.column || parentKey.length !== 1 || parentKey[0] !== reference.column) continue;\n route.parent = { column: column.name, table: reference.table, child: key[1 - i].name };\n break;\n }\n }\n return book;\n}\n\n/* ---------- ids ---------- */\n\n/* The walk's own form spells a page `@page~id`, a list `view:query` and a\n New form `!new`: an App address escapes those marks inside a value, so a\n raw one in a segment always means the older form (readAppSegments). */\nconst encode = (value: unknown) =>\n encodeURIComponent(String(value)).replace(/[!'()*~]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);\nconst OLDER_MARKS = /[@~!:]/;\n\n/** A key named column by column: `order_id=12,line=3`, each value escaped so a `,` or `=` in one is data. */\nfunction namedKey(key: Readonly<Record<string, unknown>>, columns: readonly string[]): string | undefined {\n if (!columns.length || columns.some((column) => key[column] === undefined || key[column] === null)) return undefined;\n return columns.map((column) => `${column}=${encode(key[column])}`).join(',');\n}\n\n/** The text a record's name becomes after its id: \"2021 Toyota Corolla\" → \"2021-toyota-corolla\". */\nexport function addressSlug(label: string | undefined): string {\n return label ? slugOf(label, '') : '';\n}\n\ninterface RecordAddress {\n /** The record's key: its primary key, as the walk holds it. */\n key: Readonly<Record<string, unknown>>;\n /** The natural key's value, when the route has one and it is known. */\n natural?: unknown;\n /** The record's name, for a slug where the route takes one. */\n slug?: string;\n /** The tab open on it, as its word. */\n tab?: string;\n /** The segment to say it under, when not the table's own: a split's, a second page's. */\n via?: string;\n}\n\n/**\n * A record's address after the App's id: `['leads', '127']`. Undefined when\n * the table has no route, or the key is incomplete — the caller writes the\n * walk's own form then. A natural-key route whose value is not known yet is\n * said by its primary key, named (`id=12`), which reads back without doubt.\n */\nexport function recordSegments(book: AddressBook, table: string, at: RecordAddress): string[] | undefined {\n const route = book.records.get(table);\n if (!route) return undefined;\n const pk = primaryKeyOf(book.tables.get(table));\n if (!pk.length) return undefined;\n const tab = at.tab ? [encode(at.tab)] : [];\n const segment = encode(at.via ?? route.segment);\n if (route.column) {\n if (\n at.natural !== undefined &&\n at.natural !== null &&\n String(at.natural) !== '' &&\n !String(at.natural).includes('=') &&\n String(at.natural) !== 'new'\n ) {\n return [segment, encode(at.natural), ...tab];\n }\n const named = namedKey(at.key, pk);\n return named ? [segment, named, ...tab] : undefined;\n }\n if (route.parent && at.via === undefined) {\n const parent = book.records.get(route.parent.table)!;\n const up = at.key[route.parent.column],\n down = at.key[route.parent.child];\n if (up !== undefined && up !== null && down !== undefined && down !== null) {\n return [encode(parent.segment), single(up), segment, single(down), ...tab];\n }\n }\n if (pk.length === 1) {\n const value = at.key[pk[0]];\n if (value === undefined || value === null) return undefined;\n let id = single(value, pk[0]);\n const words = route.slug && at.slug && /^\\d+$/.test(String(value)) ? at.slug : '';\n if (words) id += `-${words}`;\n return [segment, id, ...tab];\n }\n const named = namedKey(at.key, pk);\n return named ? [segment, named, ...tab] : undefined;\n}\n\n/** One id, bare — unless bare would be read as something else: `new`, or nothing. */\nfunction single(value: unknown, column?: string): string {\n const text = String(value);\n if (column && (text === 'new' || text === '')) return `${column}=${encode(text)}`;\n return encode(text);\n}\n\n/* ---------- reading ---------- */\n\ntype ParsedAddress =\n | { kind: 'route'; target: RouteTarget }\n | { kind: 'new'; target: RouteTarget & { table: string } }\n /** One of the person's conversations with the assistant: `/assistant/{id}`. */\n | { kind: 'conversation'; target: RouteTarget; id: string }\n | {\n kind: 'record';\n target: RouteTarget;\n table: string;\n key: Record<string, string>;\n natural?: { column: string; value: string };\n tab?: string;\n slugged?: boolean;\n };\n\nconst decode = (text: string): string | undefined => {\n try {\n return decodeURIComponent(text);\n } catch {\n return undefined;\n }\n};\n\n/**\n * An address after the App's id, read: the segments as the browser sent\n * them, still escaped. Undefined for anything that is not an App address —\n * a query-language segment, the walk's `@`/`~` forms — which the caller\n * reads the older way and rewrites.\n */\nexport function readAppSegments(book: AddressBook, raw: readonly string[]): ParsedAddress | undefined {\n if (!raw.length || raw.some((segment) => OLDER_MARKS.test(segment))) return undefined;\n const head = decode(raw[0]);\n /* A table's own name over a record (`/desk/incident/BLU-201`, where the\n incident's view is `tickets`) is read under the route its records are\n addressed through, and so said there — never as the older reading's\n table and question, which made a list of the id. The name alone stays\n the older reading's: `/compass/lead` is the lead table. */\n const named =\n head === undefined || raw.length < 2\n ? undefined\n : book.tables.has(head)\n ? book.records.get(head)\n : /* A page's id over a record is the same: a page that had no list view was `/deal/1270`, and a list view\n over its table, added later, moved the records to the list's address; the old one still opens them. */\n book.records.get(book.pageBase.get(head) ?? '');\n const target = head === undefined ? undefined : (book.routes.get(head) ?? (named && book.routes.get(named.segment)));\n if (!target) return undefined;\n if (raw.length === 1) return { kind: 'route', target };\n if (target.kind === 'assistant') {\n const id = raw.length === 2 ? decode(raw[1]) : undefined;\n return id ? { kind: 'conversation', target, id } : undefined;\n }\n if (target.kind === 'dashboard' || target.kind === 'form' || target.kind === 'inbox') return undefined;\n if (raw.length === 2 && raw[1] === 'new') return target.kind === 'page' ? undefined : { kind: 'new', target };\n const table = target.table;\n const read = readKey(book, table, raw[1]);\n if (!read) return undefined;\n if (raw.length === 2 || raw.length === 3) {\n const tab = raw.length === 3 ? decode(raw[2]) : undefined;\n if (raw.length === 3 && !tab) return undefined;\n return { kind: 'record', target, table, ...read, ...(tab ? { tab } : {}) };\n }\n if (raw.length > 5 || read.natural) return undefined;\n // A child keyed under this record: `/orders/12/lines/3`.\n const under = decode(raw[2]);\n const childTarget = under === undefined ? undefined : book.routes.get(under);\n const childTable = childTarget && 'table' in childTarget ? childTarget.table : undefined;\n const child = childTable ? book.records.get(childTable) : undefined;\n const pk = primaryKeyOf(book.tables.get(table));\n const down = decode(raw[3]);\n if (!childTarget || !child?.parent || child.parent.table !== table || pk.length !== 1 || down === undefined) return undefined;\n const tab = raw.length === 5 ? decode(raw[4]) : undefined;\n if (raw.length === 5 && !tab) return undefined;\n return {\n kind: 'record',\n target: childTarget,\n table: child.table,\n key: { [child.parent.column]: read.key[pk[0]], [child.parent.child]: down },\n ...(tab ? { tab } : {}),\n };\n}\n\n/** An id segment as the table's route reads it. */\nfunction readKey(\n book: AddressBook,\n table: string,\n raw: string,\n): { key: Record<string, string>; natural?: { column: string; value: string }; slugged?: boolean } | undefined {\n const route = book.records.get(table);\n const pk = primaryKeyOf(book.tables.get(table));\n if (!pk.length) return undefined;\n if (raw.includes('=')) {\n const key: Record<string, string> = {};\n for (const pair of raw.split(',')) {\n const eq = pair.indexOf('=');\n const column = decode(pair.slice(0, eq)),\n value = decode(pair.slice(eq + 1));\n if (eq <= 0 || column === undefined || value === undefined || Object.hasOwn(key, column)) return undefined;\n key[column] = value;\n }\n const names = Object.keys(key);\n if (names.length === pk.length && pk.every((column) => Object.hasOwn(key, column))) return { key };\n if (route?.column && names.length === 1 && names[0] === route.column)\n return { key, natural: { column: route.column, value: key[route.column] } };\n return undefined;\n }\n const value = decode(raw);\n if (value === undefined || value === '') return undefined;\n if (route?.column) return { key: { [route.column]: value }, natural: { column: route.column, value } };\n if (pk.length !== 1) return undefined;\n if (route?.slug) {\n const leading = /^(\\d+)(?:-.*)?$/s.exec(value);\n if (leading) return { key: { [pk[0]]: leading[1] }, ...(leading[1] !== value ? { slugged: true } : {}) };\n }\n return { key: { [pk[0]]: value } };\n}\n\n/* ---------- the check ---------- */\n\ninterface CatalogColumn extends ColumnLike {\n nullable: boolean;\n}\ninterface CatalogTable extends TableLike {\n columns: readonly CatalogColumn[];\n unique?: readonly string[];\n uniquePairs?: ReadonlyArray<readonly [string, string]>;\n}\n\n/**\n * A natural key is an address only where the database keeps it one: NOT\n * NULL and unique by a constraint or a unique index the catalog reports.\n * Two rows under one key would make a shared link open whichever the\n * database found first, and a null would have no address at all, so the\n * check refuses rather than warns. The primary key alone proves itself.\n */\nexport function naturalKeyProblems(book: AddressBook, tenantOwned: (table: string) => boolean = () => false): string[] {\n const problems: string[] = [];\n for (const route of book.records.values()) {\n if (!route.column) continue;\n const table = book.tables.get(route.table) as CatalogTable | undefined;\n const column = table?.columns.find((candidate) => candidate.name === route.column);\n if (!table || !column) continue;\n const at = `resources \"${table.name}\": \"address\" names \"${column.name}\"`;\n if (column.nullable) problems.push(`${at}, which can be null. A natural key must be NOT NULL: a record without one has no address.`);\n const pk = primaryKeyOf(table);\n /* A tenant's rows are addressed within the tenant (every read is the\n session's tenant's), so a key unique beside one other column is one:\n `UNIQUE (tenant_id, number)` gives each customer its own ticket 1.\n Which column that is, the tenant policy says, not the App; but a\n tenant-owned table's every unique index must include its tenant\n column or the deployment refuses to start (tenantUniqueRefusal), so a\n pair that holds here, there holds with the tenant column. */\n const tenant = tenantOwned(table.id);\n const proven =\n (pk.length === 1 && pk[0] === column.name) ||\n Boolean(table.unique?.includes(column.name)) ||\n (tenant && Boolean(table.uniquePairs?.some((pair) => pair.includes(column.name) && pair[0] !== pair[1])));\n if (!proven)\n problems.push(\n `${at}, which the database does not keep unique. ${\n tenant\n ? `Add a UNIQUE constraint on it within its tenant, \"UNIQUE (<tenant column>, ${column.name})\"`\n : `Add a UNIQUE constraint or a unique index on \"${column.name}\" alone`\n }, so two records can never share an address.`,\n );\n }\n return problems;\n}\n\n/* ---------- the route table ---------- */\n\n/**\n * An App mounted at the root beside other Apps (mount.ts): `/billing` is the\n * Billing App's before it is any route of this one, so a route of that name\n * could never be reached. `taken` is each other App's segment, by its id.\n */\nexport function mountClashes(book: AddressBook, taken: Readonly<Record<string, string>>): string[] {\n return Object.entries(taken).flatMap(([app, segment]) => {\n const target = book.routes.get(segment);\n return target\n ? [\n `${describe(target)}: its address \"/${segment}\" is where the App \"${app}\" is mounted, beside this one at the root. ` +\n `Give it another \"path\", or mount \"${app}\" elsewhere (--mount ${app}=/<segment>).`,\n ]\n : [];\n });\n}\n\n/**\n * Every route, one line each, as `mistri check --routes` prints it.\n * `home` is the App's home as the browser shows it (mount.ts `appHome`):\n * `/` at the root, `/compass` under its id, `/tools/` under a base path.\n */\nexport function routeTable(book: AddressBook, home: string): Array<{ path: string; what: string }> {\n const rows: Array<{ path: string; what: string }> = [{ path: home, what: 'home' }];\n const under = home.replace(/\\/$/, '');\n const records = [...book.records.values()];\n for (const [segment, target] of book.routes) {\n const at = `${under}/${segment}`;\n const what = describe(target);\n if (target.kind === 'dashboard') {\n /* The route itself, then one line per parameter it reads, each said\n only where the dashboard offers it: a room's place, a window, a pick. */\n rows.push({ path: at, what });\n if (target.params)\n rows.push({ path: `${at}?${target.params.map((name) => `${name}=…`).join('&')}`, what: `${what}, at the place it keeps` });\n if (target.window) rows.push({ path: `${at}?period=…`, what: `${what}, over one of its windows` });\n for (const column of target.picks ?? []) rows.push({ path: `${at}?${column}=…`, what: `${what}, picked by \"${column}\"` });\n for (const key of target.periods ?? [])\n rows.push({ path: `${at}?period.${key}=…`, what: `${what}, the section \"${key}\"'s own window` });\n continue;\n }\n rows.push({ path: at, what });\n if (target.kind === 'list') {\n if (target.tabs) rows.push({ path: `${at}?tab=…`, what: `${what}, on one of its tabs (${target.tabs.join(', ')})` });\n if (target.filters) rows.push({ path: `${at}?{filter}=…`, what: `${what}, refined by its filters (${target.filters.join(', ')})` });\n }\n if (target.kind === 'list' || target.kind === 'table') rows.push({ path: `${at}/new`, what: `the New form over \"${target.table}\"` });\n for (const route of records.filter((route) => route.segment === segment)) {\n const table = book.tables.get(route.table)!;\n const id = route.column\n ? `{${route.column}}`\n : route.parent\n ? undefined\n : primaryKeyOf(table).length === 1\n ? `{${primaryKeyOf(table)[0]}}${route.slug ? '[-slug]' : ''}`\n : primaryKeyOf(table)\n .map((column) => `${column}={…}`)\n .join(',');\n if (id) rows.push({ path: `${at}/${id}`, what: `one record of \"${table.name}\"` });\n if (route.parent) {\n const parent = book.records.get(route.parent.table)!;\n rows.push({\n path: `${under}/${parent.segment}/{${route.parent.column}}/${segment}/{${route.parent.child}}`,\n what: `one record of \"${table.name}\", under its \"${book.tables.get(parent.table)!.name}\"`,\n });\n }\n }\n }\n return rows;\n}\n","/**\n * The App model: the internal shape every server and client reader works\n * from (AppDef and its parts) and the enumerations the parsers and the JSON\n * schema share. The helpers every parse location uses are model.ts's, and a\n * list's presentations listPresentations.ts's: the page and list rules read\n * them too, and those rules are not the App's.\n *\n * The parse front in app.ts, with appResourceBlocks.ts, appAuthGrants.ts,\n * appViews.ts, appHome.ts, appPages.ts and appUi.ts, validates the authored\n * object and normalises it into AppDef. The data half speaks the authored\n * nouns — `sources` with their default, and one ResourceDef per declared\n * resource under its logical name, holding its New form, its actions and its\n * captions; list and dashboard views keyed by id, and the nav tree of\n * references to them. Nothing here imports another app module: they all\n * import this one.\n */\nimport type { SectionWidth } from '@mistriwork/engine/shared/page.js';\nimport type { FormRule, FormStep } from './formRules.js';\nimport type { FormStyle } from '@mistriwork/engine/shared/formValues.js';\nimport type { ThemeDefinition } from '@mistriwork/engine/shared/theme.js';\nimport type { RecordBrandingSpec } from './branding.js';\nimport type { TableSizing } from '@mistriwork/engine/shared/tableSizing.js';\nimport type { AppAction, AppFormCommandReference } from './command.js';\nimport type { AppIconName } from '@mistriwork/engine/shared/appIcons.js';\nimport type { SaidCaptions } from '@mistriwork/engine/shared/appMessages.js';\nimport { CHART_SHAPES, type AppNoun, type FigureFormat } from '@mistriwork/engine/shared/model.js';\nimport type { AppListEmpty, AppListPresentation, AppListTab, FilterBar } from '@mistriwork/engine/shared/listPresentations.js';\nimport type { FunnelStage } from '@mistriwork/engine/shared/funnel.js';\nexport type { FormScalar, FormCondition, FormRule, FormChildGroup, FormStep, AppFormField } from './formRules.js';\nexport type {\n AppAction,\n AppUpdateAction,\n AppCommandAction,\n AppCommandReference,\n AppCommandField,\n AppFormCommandReference,\n} from './command.js';\nimport {\n isServedForm,\n type AppDisplayRule,\n type AppFileDef,\n type AppFirstRunStep,\n type AppForm,\n type AppLanding,\n type AppShell,\n type AppTagsDef,\n type LandingFooter,\n type LandingImage,\n type SeriesColor,\n type ServedDashboardView,\n type ServedFormView,\n} from '@mistriwork/engine/shared/servedApp.js';\nexport type {\n AppAvatarPhoto,\n AppDisplayRule,\n AppFileBucket,\n AppFileDef,\n AppFirstRunStep,\n AppForm,\n AppLanding,\n AppShell,\n AppShellNew,\n AppSidebar,\n AppTagsDef,\n FormLines,\n LandingAction,\n LandingActionIntent,\n LandingFooter,\n LandingBadge,\n LandingHero,\n LandingHeroLayout,\n LandingPoint,\n LandingImage,\n LandingItem,\n LandingLink,\n LandingQuestion,\n LandingSearch,\n LandingSection,\n SeriesColor,\n} from '@mistriwork/engine/shared/servedApp.js';\nexport { navViewKeys, PLACEMENTS, viewOrder } from '@mistriwork/engine/shared/servedApp.js';\n\n/**\n * A list view: the rows of one resource — how they read (`show`, `tabs`,\n * `filters`), how they draw (`presentation`), how they open (`open`). Keyed\n * in `AppDef.views` by its id, which is also its address: the client keys a\n * room on the view it was opened from, and the address carries it before\n * the list's question (`/{app}/{id}:{query}`) — how a split beside a plain\n * list over one table is told apart. What a person may do with the rows —\n * the New form, the actions — is the resource's (`ResourceDef`).\n */\nexport interface AppListView {\n /** The table's id — qualified where the engine qualifies things. */\n table: string;\n /** What the list is called. Absent means the table's own name, humanised. */\n label?: string;\n /** Its address after the App's, when not its key (appAddress.ts). */\n path?: string;\n /** The menu's mark for it, one of the App icons (appIcons.ts). */\n icon?: AppIconName;\n /** With the platform realm, whose context the list serves; its menu group's by default. */\n realm?: AppRealm;\n /** The roles that may open it, beside its table grant (appViewRoles.ts). Absent is everyone's. */\n roles?: string[];\n /**\n * A module: conditions this view always stands on — \"is_urgent = true\"\n * makes the view \"Urgent orders\", a destination of its own beside the\n * plain \"Work orders\". Tabs refine on top of it. Conditions only; the\n * shown columns stay the view's `show`.\n */\n query?: string;\n /** Columns or label paths the list shows, in order. Absent means all of them. */\n show?: string[];\n /** A split's single keys, key → the id of an action of the picked record. */\n keys?: Record<string, string>;\n /** false draws the list without its title. */\n heading?: false;\n presentation?: AppListPresentation;\n /** Copy for the list as it opens (its first tab) with no rows; a tab says its own (`tabs[].empty`), and a search or\n filter past the tab keeps the no-matches message. */\n empty?: AppListEmpty;\n /** Filter tabs across the top of the list. Absent means one unfiltered list. */\n tabs?: AppListTab[];\n /**\n * Served, never written: what the menu's count counts where it is not\n * `query`. A list over a saved form's root whose tab shows the drafts keeps\n * them out of every other tab, and out of its count too (savedFormLists.ts).\n */\n countQuery?: string;\n /** The menu's count (ui.shell.counts): true counts this list, false never does; absent, the rule\n decides (shared/menuCounts.ts). */\n count?: boolean;\n /**\n * Columns offered as quick filters above the list, in order. Absent means\n * the client derives them — enum-vocabulary columns and boolean flags.\n * An empty list means none, on purpose.\n */\n filters?: string[];\n /** 'none' hides this list's filter bar; tabs remain visible. 'rail' draws the\n filters as a rail of facets beside the rows instead (a sheet on a phone). Absent: 'bar'. */\n filterBar?: FilterBar;\n /** A text filter's suggestions as the reader types: the distinct values of these columns of the\n list's own resource, under the list's standing query and the reader's grants (SuggestBox.tsx). */\n suggest?: Record<string, string[]>;\n /** Opt in to the raw query editor and Advanced filter action. Hidden by default. */\n advancedFilters?: boolean;\n /** Inline the resource's New form, subject to the ordinary create gates. */\n quickAdd?: true;\n /** The list's own CSV import, or false for none where the resource offers one (appImport.ts). */\n import?: import('./appImportDeclaration.js').AppImportDef | false;\n /** Whether the view carries a Favorites star. Opt-in. */\n favorite?: true;\n /** Numeric columns summed in a footer row over the whole query, not the page. A grid only. */\n totals?: string[];\n /** Row height; 'comfortable' is the default. */\n density?: 'comfortable' | 'compact';\n /** A ⋯ menu at the end of each row with the record's own actions. A grid only. */\n rowActions?: true;\n /** Cells edited in place, and a selection's \"Set field…\": every shown\n column the reader may write (`true`), or the columns named. A grid only. */\n editable?: true | string[];\n /** How this list's rows open: the full page (default), a drawer over\n the list, or a modal, centred; both glanced and dismissed, the page\n one click away. */\n open?: (typeof OPEN_MODES)[number];\n /** A reference column whose record a row opens instead of the row: a join table's list opens what each row is about. A grid only. */\n opens?: string;\n sizing?: TableSizing;\n /**\n * A queue's or a board's selector: the ids of its resource's actions\n * it offers, in this order. Absent means every one of them. The actions\n * themselves are the resource's (`ResourceDef.actions`).\n */\n actions?: string[];\n /** The orders a reader may choose, in a Sort menu beside the filters: each a label and a sort\n written like the query bar (\"price\", \"price desc\", \"year desc, mileage\"). Absent: no menu. */\n sorts?: Array<{ label: string; sort: string }>;\n /** The order the list reads in when nothing else says one, written like the query bar: a tab's own sort\n wins over it, and the Sort menu over both. Kept out of the bar, as a hand order is. Not beside `order`. */\n sort?: string;\n /**\n * The numeric column holding each row's place: the list reads in it,\n * ascending, wherever nothing else sorts the question, and a reader who\n * may edit it puts rows in place — a drag, Alt+↑/↓, or the row's grip.\n * A grid, a checklist, a queue or cards; a board keeps its own in its\n * presentation.\n */\n order?: string;\n}\n\n/**\n * One node of nav, as authored: a reference to a view — with the menu's own\n * word for it when it has one — or a group of nodes. Nav only arranges; the\n * views it names are `AppDef.views`.\n */\nexport type AppNavNode =\n | { view: string; label?: string; color?: SeriesColor }\n | { label: string; to: import('../../schema/app.js').StaticAppTarget; color?: SeriesColor }\n | {\n group: string;\n label: string;\n icon?: AppIconName;\n realm?: AppRealm;\n placement?: 'footer';\n inline?: true;\n folded?: true;\n children: AppNavNode[];\n };\n\nexport const AUTH_PROVIDERS = ['email', 'github', 'google'] as const;\n/** An identity provider the deployment's config names (`oidc('workforce')` is `'oidc:workforce'`, `saml('corp')`\n `'saml:corp'`), or `'oidc:*'` / `'saml:*'`: each tenant's own provider of that kind, found by the email's\n domain, in a tenant App. */\ntype OidcProvider = `oidc:${string}`;\ntype SamlProvider = `saml:${string}`;\nexport type AppProvider = (typeof AUTH_PROVIDERS)[number] | OidcProvider | SamlProvider;\n/** The name after `oidc:`: a config entry's name, lower-case, never one of the built-in providers'. */\nexport const OIDC_PROVIDER = /^oidc:([a-z][a-z0-9-]{0,39})$/;\nexport const TENANT_OIDC = 'oidc:*';\n/** The name after `saml:`, as for `oidc:`. One namespace: an oidc and a saml entry never share a name. */\nexport const SAML_PROVIDER = /^saml:([a-z][a-z0-9-]{0,39})$/;\nexport const TENANT_SAML = 'saml:*';\n/** A provider the config file names, either kind: the name after the colon. */\nexport const NAMED_PROVIDER = /^(?:oidc|saml):([a-z][a-z0-9-]{0,39})$/;\n/** Each tenant's own providers, either kind. */\nexport const TENANT_PROVIDERS: readonly string[] = [TENANT_OIDC, TENANT_SAML];\nexport const SIGN_UP_MODES = ['open', 'invite'] as const;\n/** Just-in-time sign-up: an account is made at its first sign-in through one of these providers, or with a\n verified email in one of these domains, or both when both are said. */\ninterface AppJustInTime {\n providers?: AppProvider[];\n domains?: string[];\n}\nexport type AppSignUp = (typeof SIGN_UP_MODES)[number] | AppJustInTime;\n/** A role an identity provider's claims give: a claim (a name, or a dotted path into one) and the values that\n mean the role, in `match`. Any listed value of any listed claim gives it. With `provider` (authored\n `claims: { provider: oidc('workforce'), … }`), only a sign-in through that provider reads it; without, any\n of the App's named providers does. */\nexport interface AppClaimRule {\n provider?: AppProvider;\n match: Record<string, string[]>;\n}\n/** Row scoping within the role grants: every row, a server-defined row policy, or tenant membership. */\nexport const ROW_SCOPES = ['all', 'policy', 'tenant'] as const;\nexport type AppRows = (typeof ROW_SCOPES)[number];\n\n/** Whose context something belongs to in a tenant App with the platform realm\n (`auth.tenancy.platform`): a tenant's own, or the platform realm's, which\n reads across tenants. Absent means the App's own realm, `tenant`. */\nconst REALMS = ['tenant', 'platform'] as const;\nexport type AppRealm = (typeof REALMS)[number];\nexport function parseRealm(value: unknown, at: string): AppRealm {\n if (value !== 'tenant' && value !== 'platform') throw new Error(`${at} has a \"realm\" of \"${String(value)}\". Use \"tenant\" or \"platform\".`);\n return value;\n}\n/** Whose rows a tenant App's resource holds (`auth.rows: 'tenant'`): one\n tenant's, every tenant's read-only (`global`), or the platform realm's. */\nconst RESOURCE_OWNERS = ['tenant', 'global', 'platform'] as const;\ntype ResourceOwner = (typeof RESOURCE_OWNERS)[number];\n/** What a tenant clone copies of a tenant-owned resource (`clone`): its rows\n always (`config`), or only when the clone asks for samples (`sample`). */\nexport const RESOURCE_CLONES = ['config', 'sample'] as const;\nexport type ResourceClone = (typeof RESOURCE_CLONES)[number];\n/** `auth.tenancy`: what a tenant is called, the platform's table of tenants,\n and the platform realm. */\nexport interface AppTenancy {\n /** What one tenant is called in the UI (\"Dealer\"). Default \"Tenant\". */\n label?: string;\n /** The platform-owned resource with one row per tenant: the one platform\n resource a tenant row may reference, and only by its ownership column. */\n profile?: string;\n /** Declares the platform realm; its label names the platform context (\"Lender\"). `me` is who the word is there,\n as `auth.me` says it in a tenant's: a row of the platform's table of its staff, by the column holding their\n account id. Absent, `me` in the platform realm is nobody's row. */\n platform?: { label: string; me?: { from: string; column: string } };\n}\n\n/** How people sign in: what the auth runtime captures at startup, so changing it needs a restart. */\nexport interface AppSignIn {\n /** How people sign in: email + password, and the social providers whose\n client id and secret the environment holds. Default: email. */\n providers: AppProvider[];\n /** `invite`: an admin creates accounts; `open`: anyone may sign up; `{ providers, domains }`: an account\n is made at its first sign-in through an identity provider, within those. Default: invite. */\n signUp: AppSignUp;\n /** The SQLite file sessions and accounts live in. Default: the mistri\n config directory, named for the app — never the app's own database and\n never the app directory, which is committed. */\n store?: string;\n /** The role a new account gets. Default: user. */\n defaultRole: string;\n}\n\n/**\n * `auth`: sign-in, and who may do what once signed in. Its grants are made\n * within the resource ceilings (`ResourceDef.access` and `fields`): one\n * ceiling model, with or without sign-in.\n */\nexport interface AppAuth extends AppSignIn {\n /** Row scoping within the grants. Default: all. */\n rows: AppRows;\n /** Default-deny grants within the resource ceilings, by role. Absent: a\n signed-in App with no grants, which scopes nothing. */\n roles?: Record<string, AppRoleGrant>;\n /** What a signed-out visitor reads (`auth.public`): these resources, read-only,\n narrowed by `fields`. Absent: a signed-out visitor opens only the landing\n and public forms. */\n public?: AppPublicGrant;\n /** The human check every public door asks first (`auth.challenge`): a verifier the commands module's `challenges`\n names (commands/challenges.ts). Absent: the doors rely on their ticket, honeypot and budgets. */\n challenge?: string;\n /** Tenancy (`auth.rows: 'tenant'`): what a tenant is called, its profile, and\n the platform realm. Absent: tenants only, no platform realm. */\n tenancy?: AppTenancy;\n /** Who `me` is as a row (appMe.ts): the table people are rows of, and its\n column holding their sign-in email — in a tenant App, their account's id, one row per membership.\n Absent, `me` is the email alone. */\n me?: { from: string; column: string; create?: { name?: string } };\n}\n\n/** A signed-out visitor's reads: every resource named, never `true`, and nothing written. */\nexport interface AppPublicGrant {\n read: string[];\n /** A narrowing of the column ceiling for signed-out visitors; reads only. */\n fields?: Record<string, AppFieldAccess>;\n}\n\nexport interface AppRoleGrant {\n /** The role's name as people read it, wherever a screen names the role (accounts, grants, invitations); the id otherwise. */\n label?: string;\n /** The role whose grants it holds too. Its grants below already include them (appAuthGrants.ts inheritedGrants); the\n name says where they came from. */\n extends?: string;\n flags?: readonly import('./appFlagCapabilities.js').FlagCapability[];\n experiments?: readonly import('./appFlagCapabilities.js').ExperimentCapability[];\n /** true grants the App's ceiling, never every database resource. */\n read: boolean | string[];\n write: boolean | string[];\n /** Of what it writes, what the role makes new rows in: absent, all of it;\n false, none (it edits and never creates); a list, those. */\n create?: false | string[];\n /** Tables the role may export (a CSV/JSON file of every matching row) beyond those it writes, which it may always\n export. Of what it reads. Absent: only what it writes. */\n export?: string[];\n /** Explicit server command IDs only. Absent denies; never grants generic CRUD. */\n commands?: string[];\n /** Fixed actions this role may run on a resource it reads and does not\n write, by id (`{ pay_run: ['submit'] }`). The action's own\n `set`, `when` and reason are the whole of the write; never generic CRUD. */\n actions?: Record<string, string[]>;\n /** Operator reads this role may make: readiness, the support bundle and/or the durable workflows room. Absent denies. */\n operations?: OperationsGrantAction[];\n /** A narrowing of the column ceiling for this role. */\n fields?: Record<string, AppRoleFieldAccess>;\n /** With the platform realm, whose context the role holds: a tenant's or the\n platform's. Required on every role then; roles of the two realms never\n combine. A platform role edits only the platform's own writable rows\n (appTenancy.ts) and changes a tenant's through commands. */\n realm?: AppRealm;\n /** Authored as `operations: ['support']` on a tenant role: it may read its own dealer's support sessions: who\n entered, why, when, how it ended, and the field changes this role's own\n grants already let it read. Never the operator's email, the native\n session, a version or a MAC. Declared, never defaulted. */\n supportLog?: true;\n /** Authored as `operations: ['support']` on a platform role: it may read every dealer's support sessions, the one\n view where the operator's email appears. Declared, never defaulted. */\n supportAudit?: true;\n /** The identity provider's claims that give this role, re-read at every sign-in through an `oidc:` provider\n (auth/identityRoles.ts). A role with it is the provider's to give and take away. */\n claims?: AppClaimRule;\n /** A role only a service principal holds — a jobs module's principal, an App API token — never a person: a\n free-form page is offered to it only where the page names it, and it is never the default role or a\n claim's. Declared, never inferred: the jobs module is read after the App. */\n service?: true;\n}\n\nexport interface AppFieldAccess {\n /** Local column names, not reference paths. Complete primary keys stay readable. */\n read: string[];\n write?: string[];\n}\n/** A role's narrowing: `read` absent is the resource's own read ceiling, so the role narrows only `write`. */\nexport type AppRoleFieldAccess = Omit<AppFieldAccess, 'read'> & { read?: string[] };\n\nexport const OPERATIONS_GRANT_ACTIONS = ['readiness', 'support', 'workflows', 'jobs', 'deliveries', 'stores', 'analytics'] as const;\nexport type OperationsGrantAction = (typeof OPERATIONS_GRANT_ACTIONS)[number];\n\n/** The sign-in half of `auth`, without its grants: what the auth runtime\n captures, and so what a reload may not change. Comparisons normalise\n both sides with it, so a runtime's captured settings and the App's\n `auth` are judged on the same four facts. */\nexport function signInOf(auth: AppSignIn): AppSignIn;\nexport function signInOf(auth: AppSignIn | undefined): AppSignIn | undefined;\nexport function signInOf(auth: AppSignIn | undefined): AppSignIn | undefined {\n if (!auth) return undefined;\n return {\n providers: auth.providers,\n signUp: auth.signUp,\n ...(auth.store === undefined ? {} : { store: auth.store }),\n defaultRole: auth.defaultRole,\n };\n}\n\n/**\n * A command form view: one form running one command that creates records,\n * keyed in `views` like every view. With steps, every command input\n * belongs to exactly one; without, the form is one page. Presentation only\n * — the handler, registered through `--commands`, owns validation and\n * every write.\n *\n * Signed in unless `public` is set: a room of the App, run through the\n * signed-in grant and served only to a role granted its command. Public: a\n * page at `/{app}/form/{key}` for anyone, run as one public command under\n * a public principal, even for someone signed in.\n */\nexport interface AppFormView<Field extends string = string> extends ServedFormView {\n /** The form's heading. */\n label: string;\n /** A signed-in form's address after the App's, when not its key. */\n path?: string;\n /** The menu's mark for a signed-in form; a public form is in no menu. */\n icon?: AppIconName;\n /** With the platform realm, whose context the signed-in form serves. */\n realm?: AppRealm;\n /** A signed-in form's: the roles that may open it, beside its command grant (appViewRoles.ts). */\n roles?: string[];\n /** Shown under the heading. */\n description?: string;\n /** The command the form runs: on a form saved each step (`save`), the final submit. */\n command: AppFormCommandReference;\n /** On a saved form, each step runs its own command and asks that command's inputs. */\n steps?: Array<FormStep<Field> & { command?: AppFormCommandReference }>;\n submitLabel?: string;\n /** How the steps are drawn: the stepper (default) or the intake style (a progress bar, one question at a time, a\n sticky bar of actions and an editable review). */\n style?: FormStyle;\n /** Intake: a quiet line over the progress bar. */\n note?: { icon?: AppIconName; text: string };\n /** Intake: whether the last page reviews every answer. Present only as `false`; absent, an intake form reviews. */\n review?: false;\n /** The screen after the receipt, beside the reference the handler returned when it returned one. */\n done?: { title?: string; description?: string };\n public?: true;\n /** The inputs a link's address may start filled (`?stock_ref=FC-1049`): shown, editable, checked like anything typed. */\n prefillable?: string[];\n /** Command input → the App's own input component, drawn in place of the platform's control. Never on a public form. */\n inputs?: Record<string, import('@mistriwork/engine/shared/appComponents.js').ComponentDecl>;\n /** Saved on every step into the App's real tables (appSavedForms.ts): a public form only. */\n save?: import('./appSavedForms.js').AppFormSave;\n /** A saved form's status check: a receipt confirmed by what only the applicant knows, answering a state. */\n statusCheck?: import('./appSavedForms.js').AppStatusCheck;\n /** Linking a submitted application to an account on proven control (appSavedForms.ts, forms/formLink.ts). */\n link?: import('./appSavedForms.js').AppFormLink;\n}\n\n/** A form view as served, carrying its reason when its reference inputs do not fit this catalog. */\nexport type ResolvedFormView = AppFormView & { error?: string };\n\n/** Whether a view is a command form: the one kind of view that runs a command. */\nexport const isFormView: (view: object) => view is AppFormView = isServedForm as (view: object) => view is AppFormView;\n\n/** Whether a view is a public form: a form that says so, and nothing else. */\nexport const isPublicForm = (view: object | undefined): view is AppFormView & { public: true } =>\n view !== undefined && isFormView(view) && view.public === true;\n\n/** Whether a view is a public free-form page that runs public commands: a door a signed-out visitor submits through, as a\n public form is one (http/pageCommandRoutes.ts). */\nexport const isPublicCommandPage = (view: object | undefined): view is { page: { public: true; commands: readonly string[] } } => {\n const page = (view as { page?: { public?: unknown; commands?: unknown } } | undefined)?.page;\n return page?.public === true && Array.isArray(page.commands) && page.commands.length > 0;\n};\n\nexport const LANDING_SECTION_KINDS = ['features', 'steps', 'faq', 'text', 'callToAction', 'contact', 'view', 'search'] as const;\nexport const LANDING_ACTION_INTENTS = ['primary', 'secondary'] as const;\nexport const LANDING_HERO_LAYOUTS = ['split', 'overlay'] as const;\n/** Every ceiling the landing parser holds a string or a list to, in one place. */\nexport const LANDING_LIMITS = {\n title: 120,\n heading: 120,\n text: 600,\n prose: 2000,\n label: 60,\n question: 200,\n answer: 1200,\n href: 2048,\n actions: 3,\n sections: 12,\n items: 6,\n questions: 12,\n links: 8,\n shown: 12,\n filters: 6,\n highlights: 4,\n highlight: 60,\n alt: 200,\n badge: 40,\n tiles: 4,\n points: 4,\n} as const;\n\n/**\n * The app's look, as overrides of the design tokens the UI already runs\n * on — a whitelist, not a stylesheet: an app may say what its accent is,\n * how round its corners are, and what face its data wears; it may not\n * supply a registered preset for broader visual roles. The legacy tokens\n * remain supported without requiring a full definition. The soft tint is\n * derived from `accent`, so one colour — or one for each ground,\n * `{ light, dark }` — is a complete theme.\n * `font` is the *data* face and the reader's typography preference\n * still wins over it — a brand is not a reason to overrule someone's eyes.\n */\nexport interface AppTheme {\n preset?: string;\n palette?: string;\n accent?: import('@mistriwork/engine/shared/themePresets.js').AppAccent;\n /** Corner radius for the whole UI, a CSS length: \"0\", \"10px\". */\n radius?: string;\n /** The data face, a CSS font stack. Code surfaces keep the code face. */\n font?: string;\n /** A theme per audience (themeResolve.ts): `public` for a signed-out visitor's pages, and one per role. */\n for?: import('@mistriwork/engine/shared/themeResolve.js').ThemeAudiences;\n /** Never authored: the server marks the look it tells a signed-out visitor (appLook), which the reader's pick does\n not move, so a public page is the same for everyone. */\n audience?: 'public';\n}\n\nexport const THEME_KEYS = ['preset', 'palette', 'accent', 'radius', 'font', 'for'] as const;\n\n/**\n * One section of a dashboard: a rootless metric, breakdown or rows of a table, narrowed by a\n * query, laid out with the page sections' widths. What a page composes\n * about one record, a dashboard composes about the whole table.\n */\n/** A metric's goal read from data: a measure over a table and a query, the metric's own three words.\n Resolved: `table` is the table id (the metric's when unwritten), `measure` the metric's when unwritten. */\nexport interface DashTarget {\n table: string;\n query?: string;\n measure: string;\n /** A breakdown's: the target table's column whose value matches each group's key (G76). */\n by?: string;\n /** A breakdown's: the target table's date column the reader's window narrows, as the section's `over`. */\n over?: string;\n /** A whole (`targetWhole`): the goal's query without the author's own window phrase, so the reader's window replaces\n it as it does the metric's (dashboardAsks.ts, windowed). */\n windowBase?: string;\n}\n\nexport interface AppDashSection {\n sizing?: TableSizing;\n /** A rows section's guidance when it has no row: what belongs there, and where to start. */\n empty?: AppListEmpty;\n /** Authored `empty: 'hide'` (a metric takes none): the section is left out of the dashboard while it has nothing. */\n emptyMode?: 'hide';\n kind:\n | 'metric'\n | 'breakdown'\n | 'rows'\n | 'chart'\n | 'note'\n | 'matrix'\n | 'funnel'\n | 'board'\n | 'firstRun'\n | 'custom'\n | 'activity'\n | 'find'\n | 'branding'\n | 'integrations';\n /** find: the resources one box searches by their records' names (config `resources`), served as table ids. */\n searched?: string[];\n /** find: the box's words before anything is typed. */\n placeholder?: string;\n /** activity: the resources whose record history the feed reads, each with its mark and, where the\n resource's noun is not what the feed should call it, its word. Each keeps a history, as a page's\n `history()` makes its resource keep one. */\n resources?: Record<string, AppActivityResource>;\n /** custom: the author's component (appComponents.ts). */\n component?: import('@mistriwork/engine/shared/appComponents.js').ComponentDecl;\n /** custom: its named reads of rows (config `rows`), each a rows section's words. */\n reads?: Record<string, import('@mistriwork/engine/shared/appComponents.js').DashCustomRead>;\n /** custom: its named aggregate reads (config `totals`), each a metric or breakdown in all but drawing. */\n aggregates?: Record<string, import('@mistriwork/engine/shared/appComponents.js').DashCustomTotal>;\n /** custom: its props, checked against the declaration, defaults filled. */\n props?: Record<string, unknown>;\n title?: string;\n /** firstRun: what a new App asks of its first reader, in order — each a list view to add the first\n record to. Served with each view's table, which says whether the step is done (it has a row). */\n steps?: AppFirstRunStep[];\n /** Every kind but a note is about a table. */\n table?: string;\n /** note: the words. Plain text; blank lines separate paragraphs. */\n text?: string;\n /** A curated mark: above a note's guidance, or in any other section's\n heading row, beside its title (a metric's label, a chart's, a list's). */\n icon?: AppIconName;\n /** note: one link to one of the App's rooms: a list, a dashboard or a signed-in form view (appDestinations.ts). */\n action?: import('@mistriwork/engine/shared/appLinks.js').AppLinkAction;\n /** note: 'banner', the dashboard's introduction, dressed by the look's `banner` recipe. */\n variant?: 'banner';\n /** note, a banner: its short tag over the title. */\n eyebrow?: string;\n /** note, a banner: its picture, one row of a file resource. */\n image?: LandingImage;\n /** The query language, without the leading table name. */\n query?: string;\n /** metric: the breakdown grammar's words — \"count\", \"sum amount as pipeline\". */\n measure?: string;\n /** metric: a second query for the same measure; the card shows the delta. Unsaid with an `over`, the period before\n the window (app.ts, `followsClock`). */\n compare?: string;\n /** metric: which way is good news, so the delta can say so — a fall in\n \"won\" is bad, a fall in \"overdue\" is good, and the card cannot know\n without being told. Absent means the delta stays neutral. */\n better?: 'up' | 'down';\n /** metric: draw the measure's last twelve months inside the card while no window stands. Needs `over`. A windowed\n metric draws its trend across the window unless this is false (dashboardTrend.ts, sparkText). */\n spark?: boolean;\n /** metric, breakdown, chart and matrix: how its figures are said where the measure is not one column's (a computed\n or averaged one has no display rule): percent — the figure is 0–100, said with % — number, money in the\n App's currency, or duration — the figure is seconds, said as \"10h 2m\". */\n format?: FigureFormat;\n /** metric, breakdown, chart and matrix: a word said after each figure (\"days\", \"/mo\"). */\n unit?: string;\n /** metric: money said exact, to the currency's minor digits at any size (the default: a balance is read to the\n cent), or `compact` from a thousand (\"$28.1K\"), for a revenue card glanced at. */\n figure?: 'exact' | 'compact';\n /** metric and chart: a goal — a meter and the share reached on a card, a\n rule in the ink on a line or stacked bars. Never on a donut, which has\n no scale to rule. A metric's goal may be read from data: a second\n measure, said in the metric's own words (DashTarget). */\n target?: number | DashTarget;\n /** A metric whose data `target` counts the same measure over the same table with no more conditions: the target is\n the whole this figure is a share of, said \"118 of 159 (74%)\" rather than as a goal (resolveDashboard). */\n targetWhole?: true;\n /** breakdown/chart: what to group by — \"status\", \"month opened_on\". A chart may say \"auto created_at\": the unit then\n follows the window and the data (autoBucket.ts). */\n by?: string;\n /** funnel: the stages a row passes in order, 2 to 8 (funnel.ts). */\n stages?: FunnelStage[];\n /** board: ordered subset of exact lane values (1..8). Omit to discover lanes. */\n lanes?: string[];\n /** chart: how it draws. A line wants a bucketed by; bars, a bucketed by or\n categories, one series with the current bar marked; a donut, categories;\n stacked bars, a two-key by — a bar per group of the first, divided by the second.\n scatter: a dot per group of a one-key by, placed by `x` across and `measure` up; heatmap: a two-key by\n as a grid of cells shaded by the measure; waterfall: a one-key by's values as rises and falls from zero to\n a total; treemap: a one-key by's groups as tiles sized by the measure.\n breakdown: \"bars\" (the default), or \"steps\": a card per value of the column, in its order, joined by arrows. */\n shape?: 'line' | 'bar' | 'donut' | 'stacked' | 'scatter' | 'heatmap' | 'waterfall' | 'treemap' | 'bars' | 'steps';\n /** chart, a scatter: the measure that places each dot across, in the breakdown grammar's words (\"count\"). */\n x?: string;\n /** breakdown, steps: a line under a step, by the value it counts. */\n notes?: Record<string, string>;\n /** chart, a line: \"smooth\" draws a monotone curve through the points (it never overshoots one). */\n curve?: 'smooth' | 'straight';\n /** chart, a donut: what the figure in its middle counts — \"Total visitors\" — said under it. */\n total?: string;\n /** chart: the drawing's height in CSS pixels (120 or more). Absent means\n a third of the width, capped — a sparse series on a wide screen is\n better short. */\n height?: number;\n /** The date column the dashboard's time window narrows this section by.\n Absent means the window passes this section over. */\n over?: string;\n /** The section's own windows, in the query grammar's words: the reader picks this section's period in\n its heading, apart from the dashboard's, and the first is where it starts. Needs `over`. */\n windows?: string[];\n /** Served, never authored: the section's name in the address when it has its own windows —\n `?period.lines-moved-by-day=last-7-days` — the slug of its title, one per dashboard. */\n windowKey?: string;\n /** An embed: the list works as its room does — its search, its filters and, for a reader offered one,\n its selection and bulk actions — rather than drawing read-only. */\n interactive?: true;\n /**\n * Served, never authored: the query with its own `over = last N …` phrase\n * removed, for the reader's window to stand in its place. A reader picking\n * \"last 12 months\" on a metric authored `close_date = last 90 days` means\n * twelve months, not the ninety days the two windows have in common.\n */\n windowBase?: string;\n /**\n * Served, never authored: the compare was derived from \"previous period\",\n * so the client may re-derive it against the reader's window — `last 30\n * days` picked means `previous 30 days` compared — instead of comparing the\n * reader's window with the author's.\n */\n comparePeriod?: boolean;\n /** rows: columns, label paths welcome. */\n show?: string[];\n /** rows: column path → the app's word for it, for the show entries that\n carried a label. Served beside `show`, which stays the bare paths the\n query is built from. */\n labels?: Record<string, string>;\n /** rows: query-bar text. */\n sort?: string;\n limit?: number;\n width?: SectionWidth;\n /** The area of the dashboard's `layout` this section stands in. Absent without a layout; every section names one with it. */\n area?: string;\n /** An embed (`{ kind: 'view' }`): the key of the list view this section\n draws, whose room its \"open →\" opens. A metric: the key of the list view\n over its own table that the card opens, narrowed by the metric's query. A breakdown, chart, matrix or funnel: the list its\n groups (a funnel's stages) open (`to`); absent, the one list over its table that the reader is served. Absent on every other section. */\n view?: string;\n /** A metric, breakdown, chart, matrix or funnel whose authored `to` is false: its card or its groups are text, not doorways. */\n noDrill?: true;\n}\n\n/** A view that is a screen, not a table: a named set of sections, keyed in\n `AppDef.views` by its view key. Its address is `dash-<key>`, the page\n token it rides. The payload serves the same shape resolved against a\n catalog (resolveViews): sections with resolved table ids, or a reason. */\nexport interface AppDashboardView extends ServedDashboardView {\n label: string;\n /** Its address after the App's, when not its key. */\n path?: string;\n /** The words over the sections, when not its label (which the menu and the tab keep): \"Hello, {me.name}\".\n `{me.name}` is the signed-in reader's name, said by the server (appGreeting.ts). */\n heading?: string;\n /** `heading: false`: the title is not drawn, and stays for a screen reader. */\n headingHidden?: true;\n /** A short plain-text introduction shown under the dashboard heading; it may greet `{me.name}` too. */\n description?: string;\n /** The menu's mark for it, one of the App icons (appIcons.ts). */\n icon?: AppIconName;\n /** With the platform realm, whose context the dashboard serves; its menu group's by default. */\n realm?: AppRealm;\n /** The roles that may open it (a room's too), beside its tables' grants (appViewRoles.ts). */\n roles?: string[];\n sections: AppDashSection[];\n /** An independent right-hand flow on wide screens; stacks below the main flow when narrow. */\n aside?: AppDashSection[];\n /** The sections placed on a grid: one string per row, each the same 1 to 4 space-separated cells, a cell an area's\n name (a section's `area`) or `.` for none. An area covering cells in two rows spans them; sections in one area stack\n in source order. Never with an `aside`. */\n areas?: string[];\n /** The time windows offered when any section declares `over`, in the\n query grammar's own words. Default: last 30 days / 90 days / 12 months,\n served filled in whenever any section has `over`, so the client never\n re-declares the default list. */\n windows?: string[];\n /** The window the dashboard opens on, one of `windows`; unsaid, it opens on all time. An address's `?period=` wins. */\n window?: string;\n /** How the window is picked: \"segmented\", a button per window (the default), or \"button\", one date-range button —\n the calendar, the dates the window covers, a menu of the windows — where the control row is tight. */\n windowControl?: 'segmented' | 'button';\n /**\n * Pick-filters: column names offered as selectors in the control row.\n * A pick narrows every section whose table has the column — a\n * vocabulary column becomes its values, a whole single-column\n * reference becomes the records it points at, said by name.\n */\n filters?: string[];\n /** Whether the view carries a Favorites star. Opt-in. */\n favorite?: true;\n /**\n * A free-form page (`definePage` in `pages/*.page.tsx`): the one custom\n * section drawing the whole page, the resources it reads — a reader who\n * cannot read one is not served the page — the commands it runs, the\n * address parameters it keeps its place in and the loader that reads for\n * it on the server.\n */\n page?: AppPageManifest;\n /** Served, never authored: the first problem, for the screen; `errors` has all of them. */\n error?: string;\n errors?: string[];\n}\n\n/** What a free-form page declares it reads, runs, keeps in its address and loads: its manifest, held to on both sides. */\ninterface AppPageManifest {\n reads: string[];\n commands: string[];\n params: string[];\n loader?: string;\n public?: true;\n}\n\n/** One resource an activity section reads: its mark, and its word in the feed where not its noun. */\nexport interface AppActivityResource {\n icon?: AppIconName;\n label?: string;\n}\n\nexport const DISPLAY_FORMATS = [\n 'money',\n 'percent',\n 'number',\n 'relative',\n 'date',\n 'datetime',\n 'items',\n 'duration',\n 'email',\n 'phone',\n 'url',\n 'richText',\n 'avatars',\n 'rating',\n 'color',\n 'time',\n 'month',\n 'year',\n 'json',\n 'qr',\n] as const;\n\n/** The roles an App names for people (`auth.roles.<id>.label`), for the sign-in answer: absent where none is labeled. */\nexport function roleLabelsOf(auth: { roles?: Record<string, { label?: string }> } | undefined): { roleLabels?: Record<string, string> } {\n const labeled = Object.entries(auth?.roles ?? {}).flatMap(([role, grant]) =>\n typeof grant?.label === 'string' ? [[role, grant.label] as const] : [],\n );\n return labeled.length ? { roleLabels: Object.fromEntries(labeled) } : {};\n}\n\n/** Whether the App names roles at all: a refusal says \"your role\" only where one could be the reason. */\nexport function keepsRoles(auth: { roles?: Record<string, unknown> } | undefined): { hasRoles?: true } {\n return Object.keys(auth?.roles ?? {}).length ? { hasRoles: true } : {};\n}\n\n/**\n * What an App's source says that the platform would say without it, found\n * where the raw root is still in hand (the parsed App cannot tell a stated\n * default from an implied one). Read by `check` (appWarnings.ts).\n */\nconst restatements = new WeakMap<object, readonly string[]>();\nexport const noteRestated = (app: object, notes: readonly string[]): void => {\n if (notes.length) restatements.set(app, notes);\n};\nexport const restatedOf = (app: object): readonly string[] => restatements.get(app) ?? [];\n\nexport interface AppSourceBinding {\n /** A server-configured connection ID or name, never a connection URL. */\n connection: string;\n /** The source whose resources stay bare. Beside others, at most one says\n so, and none has to; a source that stands alone is it unless it says\n `default: false`, which keeps its `source.name` names as others go. */\n default: boolean;\n /** `'platform'`: a platform-only source, read by a tenant App's platform\n realm alone, never the default and never written (appTenancy.ts). */\n realm?: 'platform';\n /** The one engine schema (a PostgreSQL schema) this source\n sees of its connection. Several sources may share a connection this way;\n each sees its own schema's tables and nothing else. */\n schema?: string;\n /** The tables this source brings into the App without a block each:\n `'all'`, or their local names. Each is an implicit `{}` block, which\n grants nothing a block would not. */\n resources?: 'all' | readonly string[];\n}\n\n/**\n * One logical resource, as its block declares it: where its rows live and\n * what the App says about its values. Keyed in `AppDef.resources` by the\n * logical name — bare for the default source (or the single connection),\n * `source.name` for a secondary source — exactly as authored.\n */\n/**\n * A resource searched through a search provider (server/app/searchProvider.ts):\n * the columns the index holds, and the provider's connection by its name. The\n * index answers keys; rows are always re-read under the reader's own grants.\n */\nexport interface AppSearchDef {\n connection: string;\n fields: string[];\n}\n\n/**\n * What the durable support write trail keeps of a resource's rows. Under a\n * support session (\"enter as dealer\") every Save, New, Delete, Undo and\n * command appends one audit entry in the business write's own transaction; a\n * column named here is recorded as a keyed hash instead of its value, in the\n * change and in a delete's before image alike, and every other column keeps\n * its value. `'values'` marks every column of the resource.\n */\nexport interface AppAuditDef {\n redact: 'values' | string[];\n}\n\nexport interface ResourceDef {\n /** The physical table in its source's catalog, when the name alone does not find it. */\n physical?: string;\n /** The named source the resource lives in; absent for an App without `sources`. */\n source?: string;\n /** The App's ceiling for this resource. A declared resource reads; `'write'`\n is the one that is written. With role grants, roles grant within it. An\n App serves nothing it does not declare. */\n access?: 'write';\n /** In a tenant App (`auth.rows: 'tenant'`), whose rows these are: one\n tenant's, every tenant's read-only, or the platform realm's. Required on\n every resource there; refused anywhere else. */\n owner?: ResourceOwner;\n /** In a tenant App, what a clone of a tenant into a sandbox copies of this\n tenant-owned resource: `config` rows always, `sample` rows only when the\n clone asks for samples. Unmarked, nothing (tenantClone.ts). */\n clone?: ResourceClone;\n /** `false`: on an App with `live: true`, a write to this resource's table is not announced and no page follows\n it (a hot or log-like table); its pages refresh on focus and after their own saves. Absent: the App's choice. */\n live?: false;\n /** Whether the App deletes this resource's records. Opt-in: absent, the\n server refuses a delete and no Delete is drawn (appDeletable). */\n delete?: true;\n /** Whether the resource's records carry a Favorites star. Opt-in. */\n favorite?: true;\n /** A settings record: the resource's one row, opened from the reader's\n settings (the account menu) under these words. */\n settings?: string;\n /** What one record is called and several: the New button, a split's detail heading, a page's\n label and a list's when they say none. Absent, the table's own name. */\n noun?: AppNoun;\n /** The column ceiling within `access`. Complete primary keys stay readable. */\n fields?: AppFieldAccess;\n /** Columns no reader of the App is ever served, signed in or not: cut from the catalog\n the server answers with, so every read path refuses them as columns that are not\n there. Intersects every role's field grant; never the primary key. */\n hidden?: string[];\n /** Soft keys: column → the resource it names and the column it is looked up by (absent: the target's\n primary key). Joined like a foreign key within one connection (declaredReferences.ts), read in two steps into\n another (appStitchedReads.ts); never authority for a write. */\n references?: Record<string, { to: string; column?: string }>;\n /** Column → how its values dress, everywhere they appear. */\n display?: Record<string, AppDisplayRule>;\n /** Column → the kind of value a TEXT column holds, where its declared type cannot say: `{ due_on: 'date' }`\n for ISO dates, so windows, timelines, calendars, date presets and date filters take it. SQLite only; the\n served catalog says the column is a date, and the database still holds text (declaredDates). */\n types?: Record<string, DeclaredDateKind>;\n /** Column → the App's own input component, drawn in its edit and New forms in place of the platform's control. */\n inputs?: Record<string, import('@mistriwork/engine/shared/appComponents.js').ComponentDecl>;\n /** Resource invariants, whichever form, action or command submits the write.\n Runtime activation requires authoritative writer enforcement. */\n validation?: FormRule[];\n /**\n * Under a row policy, the database triggers on this resource's table the App accepts: a command's write that fires\n * one runs, and each row its keyed effects reach is proven inside the person's rows before and after the statement\n * (docs/trigger-effects.md). The effects are read from the trigger, never declared; authority.lock records each\n * named trigger's definition, and a changed one is refused until the lock is approved again. A trigger that only\n * validates needs no name; one that is not named and writes stays refused.\n * @preview\n */\n triggers?: string[];\n /** Column → how long its text may be and what form it takes (textRules.ts): judged as the form is typed and on\n every write, over what the database already says (`ColumnMeta.text`). */\n text?: Record<string, import('@mistriwork/engine/shared/textRules.js').TextRule>;\n /** JSON column → the resource whose rows define its fields (definedFields.ts): drawn, edited, filtered and judged as fields. */\n defined?: Record<string, import('@mistriwork/engine/shared/definedFields.js').DefinedBinding>;\n /** Flows whose steps are rows (appFlows.ts): the tables they are kept in, and what their steps may do to a record. */\n flows?: import('./appFlows.js').FlowBinding;\n /** Column or label path → the App's caption for it, wherever the column is shown. */\n labels?: Record<string, string>;\n /** The path that names one record. Unlike a caption, it may follow a reference.\n `false` is an internal restricted-catalog sentinel; authors can only write a string. */\n recordLabel?: string | false;\n /**\n * The curated \"New\" form: which columns to ask for (the rest are left to\n * the database or to `defaults`), and values filled in before anyone\n * types. Absent means the form the schema writes — every column.\n */\n form?: AppForm;\n /**\n * Buttons on this resource's record page: an update the App has decided\n * in advance (\"Mark working\" sets `status` to `working`), or a server\n * command. Shown only when `when` matches the record, confirmed first\n * when `confirm` says so. A fixed update goes through the same route,\n * gate and row lock as any edit, so it is scoped like one.\n * Keyed by id where authored and served in that order, each carrying its\n * `id`: a header, a queue and a role name an action by it. Absent when the\n * resource declares none.\n */\n actions?: AppAction[];\n /** The rows are files (appFileRules): the bytes, the name, and where an upload's type and size go. */\n file?: AppFileDef;\n /** Rows from a CSV file (appImport.ts): new records through the ordinary insert, or one command per row. */\n import?: import('./appImportDeclaration.js').AppImportDef;\n /** The records carry tags, rows of `to` linked through `through` (appTagRules). */\n tags?: AppTagsDef;\n /** Search through a search provider's connection: the columns the index holds. */\n search?: AppSearchDef;\n /** The record's states and verbs (appMachine.ts): its column moves only along the graph, on every door, and its\n verbs lead the resource's `actions`. */\n machine?: import('./appMachine.js').AppMachineDef;\n /** Timers at an instant a column holds (appAt.ts): a command run through the jobs once its time has come and `when` holds. */\n at?: import('./appAt.js').AppAtTimer[];\n /** The page this resource's records open through when two pages share its\n base. The loader also orders that page first among its base's pages\n (orderAppPages), for the readers that take the first. */\n page?: string;\n /** The list view its records are addressed through (appAddress.ts). */\n view?: string;\n /** The natural key column its records are addressed by, instead of the primary key. */\n address?: string;\n /** A slug after a numeric id on public pages. */\n slug?: true;\n /** What the support write trail keeps of these rows: which columns are\n recorded as a keyed hash rather than their value. */\n audit?: AppAuditDef;\n /** Numbers about the rows that point at each record, read like columns\n (appRollups.ts): `line_total` → the sum of its lines' amounts. */\n rollups?: Record<string, AppRollup>;\n /** Virtual columns, each a typed expression over the record's own columns, read like columns and never\n written (computed.ts, appComputed.ts): `is_overdue` → `due_on < today and not completed`. */\n computed?: Record<string, AppComputed>;\n /** Which writes tell which people, kept in each person's inbox (appNotify.ts). */\n notify?: import('./appNotify.js').AppNotifyRule[];\n /** Named queries over the resource's rows (appSegments.ts): a tab, a metric, a related list or a rollup names\n one, and the query language reads one as a word (`overdue and mine`). */\n segments?: Record<string, AppSegment>;\n /** The parent balances a row of this resource uses up (appConsumes.ts): a write of the row moves them in its\n own transaction, and refuses one that would take a balance below zero. */\n consumes?: AppConsumes[];\n /** The parent balances a row of this resource adds to (a receipt's line, a return): `consumes` the other way round. */\n restocks?: AppConsumes[];\n}\n\n/** A segment as authored: its conditions in the query language, and what a tab over it is called. */\nexport interface AppSegment {\n query: string;\n /** Its caption: the authored `label`, else its key said as words (`on_bench` → \"On bench\"). */\n label: string;\n}\n\n/**\n * One `consumes` rule, on the line that uses the stock: `column` is one reference of the line and a number\n * column of the record it names (`part_id.on_hand`); `by` a number column of the line, or one per line where\n * absent. `message` is what a refused write says, its `{placeholders}` read from the parent as it stands.\n */\nexport interface AppConsumes {\n /** The line's reference column (`part_id`). */\n reference: string;\n /** The parent's balance column (`on_hand`). */\n balance: string;\n by?: string;\n message?: string;\n /** false: the balance may go below zero (a backorder). */\n floor?: false;\n}\n\n/**\n * A rollup as authored: `count('task', { whose: 'account_id', query: 'status = open' })`,\n * `sum('order_line', 'amount')`. `from` is a resource whose rows point at\n * this one through one reference (`whose`, needed only when there are two),\n * or through two (`whose: 'task_id.project_id'`, appRollups.ts);\n * `query` narrows which of them count, over their own columns.\n */\nexport interface AppRollup {\n fn: import('@mistriwork/engine/shared/rollups.js').RollupFn;\n from: string;\n column?: string;\n whose?: string;\n query?: string;\n /** A `latest`'s order: the column of `from` whose greatest value is the latest row; its key when absent. */\n by?: string;\n}\n\n/** A computed column as authored: what it is, and its expression in the query language's words. */\nexport interface AppComputed {\n type: import('@mistriwork/engine/shared/computed.js').ComputedType;\n value: string;\n}\n\n/** Existing-record save preference; grants no write capability. */\nexport interface AppEditing {\n save: 'explicit' | 'blur';\n}\n\n/** Normalized chrome and formatting. The server-only branding descriptor is\n separated from its static fallback theme during parsing. */\n/** `ui.timezone` read per reader: a text column of a resource, `resource.column`, and the App's own zone. */\nexport interface AppReaderZone {\n column: string;\n default?: string;\n}\n\n/**\n * An App's own logo picture (`ui.logo: { src, dark, mark }`): files in its directory, and, once the load has read\n * them, where each is served (`href`, `darkHref`, `markHref`; server/app/appBrand.ts).\n */\nexport interface AppLogoImage {\n src: string;\n dark?: string;\n mark?: string;\n href?: string;\n darkHref?: string;\n markHref?: string;\n}\n\nexport interface AppUi {\n /** Explicit authored choices; omission never infers an App menu. */\n /* Served said: every caption is the reader's words by now (server/app/appMessages.ts). */\n appSwitcher?: SaidCaptions<import('../../schema/app.js').AppSwitcher>;\n /** The App's footer, drawn under every room: a line of small print and a\n row of links. The landing wears it too. */\n footer?: LandingFooter;\n /** Decorative brand mark drawn beside the App name: an icon, or the App's own picture. The name remains its\n accessible label. */\n logo?: AppIconName | AppLogoImage;\n theme?: AppTheme;\n /** Internal only: authored as ui.theme: fromRecord(...), never as ui.branding.\n Bind against the logical catalog before reading an authorized row. */\n branding?: RecordBrandingSpec;\n /** Registered themes, offered in the reader's theme picker. */\n customThemes?: ThemeDefinition[];\n /** false hides the Export menu everywhere the app curates, and the server refuses the file as well, over any role's export grant. Default true. */\n export?: boolean;\n /** true makes the App an installable PWA (app/appPwa.ts): its pages carry its manifest and worker registration, and it serves its worker, icons and offline page. Preview: the shape may change in any 1.x release. Absent or false, it serves none of those. */\n pwa?: boolean;\n /** App shell visibility and spacing; omitted settings keep the existing UI. */\n shell?: AppShell;\n /** Omission keeps explicit Save. Workbench always uses explicit review/apply. */\n editing?: AppEditing;\n /** BCP 47 tag every figure the app writes is in — numbers, money, dates, counts (\"en-AU\",\n \"de-DE\") — and the language its captions are said in (messages/<locale>.ts). Absent means\n the reader's browser decides the figures and the source catalog the words. Authored as\n `{ column, default }`, this is `default`, and served it is the reader's own (appLocale.ts). */\n locale?: string;\n /** `ui.locale: { column: 'person.locale' }`: the text column each reader's own tag is read from\n (`resource.column`, their first visible row). Never served. */\n localeColumn?: string;\n /** `ui.messages: fromRecord(...)`: a tenant's own words over the App's catalogs. Never served. */\n messages?: import('./branding.js').RecordMessagesSpec;\n /** IANA zone the app's \"today\" is reckoned in (\"Australia/Sydney\"). Date\n phrases — last 30 days, this month — are windows on this calendar.\n Absent means the server's own clock. A TIMESTAMP or DATETIME that carries\n no zone holds the wall clock in this zone (UTC when the App names none) — whatever\n writes it: an action's `now`, a machine's `since`, a `Date` a command\n writes, a New form's `now`. A column with a zone (timestamptz) holds the instant.\n `{ column: 'desk_setting.timezone', default }` reads each reader's days in the\n zone their first visible row of that column names (a tenant's, an account's);\n writes keep `default`, which stands in where the row names none. Read it\n through `appZone` (appUi.ts), never directly. */\n timezone?: string | AppReaderZone;\n /** ISO 4217 default for every money rule that names none. Defaults to USD. */\n currency?: string;\n /** The origins the page opens beside its own for the App's custom components. Opening egress is authority, so\n authority.lock records each. */\n csp?: {\n /** Origins they may send requests to (`https://api.example.com`): the page's `connect-src`. */\n connect?: string[];\n /** Origins they may load pictures from: the page's `img-src`. */\n images?: string[];\n };\n}\n\nexport interface AppDef {\n /** The words a value component's `label` gave its column's vocabulary, by \"resource.column\" (server/app/valueLabels.ts):\n learned against the catalog the App is served on, never authored, and shared by reference with the definition\n said in a reader's other words so one learning serves every reader. */\n valueLabels?: Record<string, Record<string, string>>;\n /** authority.lock's approved trigger digests, by \"resource.trigger\" (server/commands/triggerLock.ts): read with the\n lock when the App loads, never authored; absent where the App keeps no lock. */\n approvedTriggers?: Record<string, string>;\n /** Private control declarations; browser payloads serve decisions separately. */\n flags?: import('./appFlags.js').FlagCatalog['flags'];\n /** Named audiences that flag rules and experiment eligibility reach by name (appFlags.ts AudienceDefinition). */\n audiences?: import('./appFlags.js').FlagCatalog['audiences'];\n experiments?: Readonly<Record<string, import('./appExperiments.js').ExperimentDeclaration>>;\n /** What the App's behavior analytics collect, emit and funnel; off when absent (appAnalytics.ts). */\n analytics?: import('./appAnalytics.js').AnalyticsDeclaration;\n /** The in-App assistant, offered to signed-in people under their own grants (appAssistant.ts, docs/assistant.md). */\n assistant?: import('./appAssistant.js').AppAssistant;\n /** How long the runtime keeps its history, command and attempt rows; absent windows are their defaults (appRetention.ts). */\n retention?: import('./appRetention.js').AppRetention;\n /** Explicit integration exposure; checked against the logical catalog before loading. */\n api?: import('./appApi.js').AppApiDef;\n /**\n * What the App's commands keep, unless a command says otherwise: `none`,\n * `receipts` (a durable receipt, so a retried request is answered once) or\n * `audit` (the receipt and every change a command made). Whatever is\n * declared, a command reached by a door that retries — the App API, MCP, a\n * job — keeps at least receipts. Absent: none for the browser's own\n * requests.\n */\n track?: 'none' | 'receipts' | 'audit';\n /**\n * Whether the App's open pages follow other people's changes (live sync):\n * on only when it says so. The operator's `--no-live` turns it off whatever\n * the App says. A resource leaves it with its own `live: false`.\n */\n live?: true;\n id: string;\n name: string;\n /** What a phone's bar and an installed icon say where `name` does not fit; 12 characters or fewer. */\n shortName?: string;\n /** Connection this app is for, by id or name. Absent means every connection. */\n connection?: string;\n /** Every resource the App declares, by logical name. A physical binding\n is optional: an undeclared binding is inferred from a unique name. */\n resources?: Record<string, ResourceDef>;\n /** Read-only resources over tables that share a shape, each read in one order and one page (appUnions.ts). */\n unions?: readonly import('./appUnions.js').AppUnion[];\n /** Named server-side bindings; connection is normalized to the default binding. */\n sources?: Record<string, AppSourceBinding>;\n /**\n * The App's views by key: the rooms a signed-in visitor opens — every\n * list, dashboard and signed-in form view nav references, every one a\n * home rule names (the one room of an App without nav), and every list\n * view a dashboard embeds, which its \"open →\" opens though no menu lists\n * it — and every public form, served at its own address.\n */\n views: Record<string, AppListView | AppDashboardView | AppFormView>;\n /** The menu as authored: references to `views`, and groups of them. Empty\n for an App without nav, which draws no menu. */\n nav: AppNavNode[];\n /** Where each visitor starts, as authored: rules read in order, the first\n that matches wins, and the last matches everyone. The server evaluates\n them per request (appHome.ts resolveHome); a view id is one rule. */\n home: AppHomeRule[];\n /** Chrome and formatting, as authored under `ui`. Always present; empty\n when the App sets none of it. */\n ui: AppUi;\n /**\n * Sign-in for this app, through Better Auth, and its role grants. Absent\n * means the server's own trust model (loopback, or a proxy header).\n * Present, every request to the app needs a session, and every write is\n * journaled with who made it.\n */\n auth?: AppAuth;\n /** The front page, under its view key: content only, no data, served\n signed out. Requires auth. */\n landing?: { view: string; content: AppLanding };\n /** What each tenant may change about how the App looks to its people, and who may change it (tenantBranding.ts). */\n branding?: import('./tenantBranding.js').BrandingDeclaration;\n}\n\n/** One home rule: `signedOut` matches a visitor with no session, `roles` a\n signed-in one holding any of them, and a rule with neither everyone. */\nexport interface AppHomeRule {\n view: string;\n signedOut?: true;\n roles?: string[];\n realm?: AppRealm;\n}\n\n/* The enumerations the parsers check against, exported so the JSON schema is\n pinned to them (test/schema.test.ts) instead of repeating them by hand. */\nexport const DASH_KINDS = [\n 'metric',\n 'breakdown',\n 'rows',\n 'chart',\n 'note',\n 'matrix',\n 'funnel',\n 'board',\n 'firstRun',\n 'custom',\n 'activity',\n 'find',\n 'branding',\n 'integrations',\n] as const;\n/** How many changes an activity section draws before \"Show more\", at most, and how many one read answers. */\nexport const ACTIVITY_MAX = 50;\n/** How many steps a first run asks: a start, not a manual. */\nexport const FIRST_RUN_MAX_STEPS = 6;\nexport const OPEN_MODES = ['page', 'drawer', 'modal'] as const;\nexport { FILTER_BARS, type FilterBar } from '@mistriwork/engine/shared/listPresentations.js';\nexport const SHELL_WIDTHS = ['full', 'wide', 'narrow'] as const;\nexport const SHELL_HEADER_WIDTHS = ['full', 'content'] as const;\nexport const SHELL_GUTTERS = ['none', 'compact', 'normal', 'comfortable'] as const;\n/** A dashboard's charts draw these as well; a record page's chart keeps to CHART_SHAPES. */\nexport const DASH_CHART_SHAPES = [...CHART_SHAPES, 'scatter', 'heatmap', 'waterfall', 'treemap'] as const;\nexport const BETTER = ['up', 'down'] as const;\n\n/** The windows offered when a dashboard declares none of its own. */\nexport const DEFAULT_WINDOWS = ['last 30 days', 'last 90 days', 'last 12 months'];\n\n/**\n * A list view as served: the table re-addressed to its resolved id, the\n * label always filled in, and a view whose table or columns this connection\n * does not have carries its reason instead of vanishing.\n */\nexport interface ResolvedListView extends Omit<AppListView, 'label'> {\n label: string;\n error?: string;\n}\n\n/** The source whose resources stay bare: the one that says `default: true`, if one does. */\nexport function defaultSourceOf(app: Pick<AppDef, 'sources'>): string | undefined {\n for (const [name, source] of Object.entries(app.sources ?? {})) if (source.default) return name;\n return undefined;\n}\n\n/**\n * The App's own source: the connection it is served under, where its\n * accounts, command journal and editing history live. Its default source,\n * or, when none is the default, the first of its own sources. Separate from\n * naming, so an App whose sources all keep `source.name` still has a home.\n * Undefined for an App without named sources.\n */\nexport function homeSourceOf(app: Pick<AppDef, 'sources'>): string | undefined {\n return defaultSourceOf(app) ?? ownSourceNames(app)[0];\n}\n\n/** The platform-only sources (`realm: 'platform'`), in declaration order: none for most Apps. */\nexport function platformSourceNames(app: Pick<AppDef, 'sources'>): string[] {\n return Object.entries(app.sources ?? {})\n .filter(([, source]) => source.realm === 'platform')\n .map(([name]) => name);\n}\n\n/** The App's own sources: every source but its platform-only ones. What has\n * one catalog counts these — the API's, and a tenant App's one source — since\n * a platform-only source is read by the platform realm's reads alone. */\nexport function ownSourceNames(app: Pick<AppDef, 'sources'>): string[] {\n return Object.keys(app.sources ?? {}).filter((name) => app.sources![name].realm !== 'platform');\n}\n\n/** A source's table under its logical name: bare in the default source, `source.name` in any other. */\nexport function qualifyResource(defaultSource: string | undefined, source: string, local: string): string {\n return source === defaultSource ? local : `${source}.${local}`;\n}\n\n/** The logical name of a source's table in this App, which a command, a catalog and a grant all speak. */\nexport function logicalResource(app: Pick<AppDef, 'sources'>, source: string, local: string): string {\n return qualifyResource(defaultSourceOf(app), source, local);\n}\n\n/**\n * One catalog's declared physical bindings, local name → physical table id:\n * those of `source`, or, without one, those of an App without named sources.\n * An undeclared binding is the catalog's to infer from a unique name.\n */\nexport function physicalBindings(app: Pick<AppDef, 'resources'>, source?: string): Record<string, string> {\n const bindings: Record<string, string> = {};\n for (const [name, resource] of Object.entries(app.resources ?? {})) {\n if (resource.physical === undefined || resource.source !== source) continue;\n bindings[name.slice(name.indexOf('.') + 1)] = resource.physical;\n }\n return bindings;\n}\n\n/** The source a reference's target belongs to: its resource block's, as parsing recorded it. */\nconst targetSource = (app: Pick<AppDef, 'resources'>, to: string): string | undefined => app.resources?.[to]?.source;\n\n/** The App's declared references within one source (the whole App, without sources), in that source's local names. */\nexport function declaredReferences(\n app: Pick<AppDef, 'resources'>,\n source?: string,\n): import('@mistriwork/engine/shared/declaredReferences.js').DeclaredReference[] {\n const local = (name: string) => name.slice(name.indexOf('.') + 1);\n return Object.entries(app.resources ?? {}).flatMap(([name, resource]) =>\n resource.source !== source\n ? []\n : Object.entries(resource.references ?? {})\n .filter(([, reference]) => targetSource(app, reference.to) === source)\n .map(([column, reference]) => ({\n table: local(name),\n column,\n to: local(reference.to),\n ...(reference.column ? { toColumn: reference.column } : {}),\n })),\n );\n}\n\n/**\n * The App's declared references from one source to another, in the App's\n * resource names (`stock_move` → `catalog.product`). Joined in SQL where the\n * two sources share a connection, read in two steps where they do not\n * (appSourceCatalog.ts).\n */\nexport function crossSourceReferences(\n app: Pick<AppDef, 'resources'>,\n): import('@mistriwork/engine/shared/declaredReferences.js').DeclaredReference[] {\n return Object.entries(app.resources ?? {}).flatMap(([name, resource]) =>\n Object.entries(resource.references ?? {})\n .filter(([, reference]) => targetSource(app, reference.to) !== resource.source)\n .map(([column, reference]) => ({\n table: name,\n column,\n to: reference.to,\n ...(reference.column ? { toColumn: reference.column } : {}),\n })),\n );\n}\n\n/** What a declared type says a TEXT column holds: ISO dates (`2026-09-24`) or date-times (`2026-09-24 14:30:00`). */\ntype DeclaredDateKind = 'date' | 'datetime';\n/** A text column an App declares a date, in its source's local names. */\nexport interface DeclaredDate {\n table: string;\n column: string;\n as: DeclaredDateKind;\n}\n\n/** The App's `types` declarations within one source (`undefined`: the App's only source), in local names. */\nexport function declaredDates(app: Pick<AppDef, 'resources'>, source?: string): DeclaredDate[] {\n const local = (name: string) => name.slice(name.indexOf('.') + 1);\n return Object.entries(app.resources ?? {}).flatMap(([name, resource]) =>\n resource.source !== source ? [] : Object.entries(resource.types ?? {}).map(([column, as]) => ({ table: local(name), column, as })),\n );\n}\n\n/** The App grants by role: sign-in with default-deny role grants within the resource ceilings. */\nexport function grantsByRole(app: Pick<AppDef, 'auth'>): boolean {\n return app.auth?.roles !== undefined;\n}\n\n/** Rows scoped by a server-defined row policy (`auth.rows: 'policy'`). */\nexport function isRowPolicyApp(app: Pick<AppDef, 'auth'>): boolean {\n return app.auth?.rows === 'policy';\n}\n\n/** Rows scoped by tenant membership (`auth.rows: 'tenant'`). */\nexport function isTenantApp(app: Pick<AppDef, 'auth'>): boolean {\n return app.auth?.rows === 'tenant';\n}\n\nexport { FILE_MAX_BYTES_CEILING, FILE_MAX_BYTES_DEFAULT } from '@mistriwork/engine/shared/fileBounds.js';\n\n/**\n * The resource ceilings by logical name, in declaration order: `read` is\n * every declared resource (a resource reads unless it says otherwise),\n * `write` those whose access is 'write',\n * `fields` the column ceilings. What the resources say, unchecked; the\n * resolver re-checks it (checkedGrants) before any grant is judged.\n */\nexport function resourceCeiling(app: Pick<AppDef, 'resources'>): {\n read: string[];\n write: string[];\n fields?: Record<string, AppFieldAccess>;\n} {\n const declared = Object.entries(app.resources ?? {});\n const fielded = declared.filter(([, resource]) => resource.fields);\n return {\n read: declared.map(([name]) => name),\n write: declared.filter(([, resource]) => resource.access === 'write').map(([name]) => name),\n ...(fielded.length ? { fields: Object.fromEntries(fielded.map(([name, resource]) => [name, resource.fields!])) } : {}),\n };\n}\n\n/** Every resource's invariants by logical name, or undefined when none declares any. */\nexport function resourceValidation(app: Pick<AppDef, 'resources'>): Record<string, FormRule[]> | undefined {\n const declared = Object.entries(app.resources ?? {}).filter(([, resource]) => resource.validation);\n return declared.length ? Object.fromEntries(declared.map(([name, resource]) => [name, resource.validation!])) : undefined;\n}\n\n/**\n * Why a key the types declare ahead of its feature is refused. Each such\n * key is parsed and refused with one of these, so an author learns the\n * feature is not built yet rather than that the key is a typo. The keys\n * are the work that turns each one on; the texts never name it.\n */\nexport const LATER = {\n T8: 'is not supported yet: it needs support sessions',\n} as const;\n\n/** The keys each authored section kind takes. Anything else is refused by\n name. Exported so the JSON schema's per-kind objects are pinned to it. A\n board's card is `primary` plus `lines` (the one card vocabulary); it is\n read into the section's `show`. A board takes no `width`: its lanes\n always have the dashboard's whole width. A `segment` is read before the\n section is (appSegments.ts unfoldSegments), as its word in `query`. */\nexport const SECTION_KEYS: Record<AppDashSection['kind'], readonly string[]> = {\n metric: [\n 'kind',\n 'area',\n 'title',\n 'icon',\n 'from',\n 'query',\n 'segment',\n 'measure',\n 'compare',\n 'better',\n 'spark',\n 'over',\n 'windows',\n 'width',\n 'target',\n 'to',\n 'format',\n 'unit',\n 'figure',\n ],\n breakdown: [\n 'kind',\n 'area',\n 'title',\n 'icon',\n 'from',\n 'query',\n 'segment',\n 'measure',\n 'by',\n 'shape',\n 'notes',\n 'over',\n 'windows',\n 'limit',\n 'width',\n 'format',\n 'unit',\n 'target',\n 'to',\n 'empty',\n ],\n chart: [\n 'kind',\n 'area',\n 'title',\n 'icon',\n 'from',\n 'query',\n 'segment',\n 'measure',\n 'x',\n 'by',\n 'shape',\n 'curve',\n 'total',\n 'over',\n 'windows',\n 'limit',\n 'width',\n 'height',\n 'target',\n 'format',\n 'unit',\n 'to',\n 'empty',\n ],\n funnel: ['kind', 'area', 'title', 'icon', 'from', 'query', 'segment', 'stages', 'over', 'windows', 'width', 'to', 'empty'],\n matrix: [\n 'kind',\n 'area',\n 'title',\n 'icon',\n 'from',\n 'query',\n 'segment',\n 'measure',\n 'by',\n 'over',\n 'windows',\n 'width',\n 'format',\n 'unit',\n 'to',\n 'empty',\n ],\n rows: [\n 'kind',\n 'area',\n 'title',\n 'icon',\n 'from',\n 'query',\n 'segment',\n 'show',\n 'sort',\n 'limit',\n 'over',\n 'windows',\n 'width',\n 'sizing',\n 'empty',\n ],\n board: ['kind', 'area', 'title', 'icon', 'from', 'query', 'segment', 'by', 'lanes', 'primary', 'lines', 'sort', 'limit', 'over', 'empty'],\n note: ['kind', 'area', 'title', 'text', 'icon', 'action', 'width', 'variant', 'eyebrow', 'image'],\n firstRun: ['kind', 'area', 'title', 'description', 'steps'],\n custom: ['kind', 'area', 'title', 'width', 'component', 'rows', 'figures', 'props'],\n activity: ['kind', 'area', 'title', 'icon', 'resources', 'limit', 'width'],\n find: ['kind', 'area', 'title', 'icon', 'resources', 'placeholder', 'width'],\n branding: ['kind', 'area', 'title', 'icon', 'width'],\n integrations: ['kind', 'area', 'title', 'icon', 'width'],\n};\n\n/** The keys a dashboard's embed of a list view takes (`{ kind: 'view', view }`). */\nexport const VIEW_SECTION_KEYS = [\n 'kind',\n 'view',\n 'area',\n 'title',\n 'icon',\n 'width',\n 'tab',\n 'limit',\n 'over',\n 'windows',\n 'interactive',\n 'empty',\n] as const;\n","import type { CommandFieldPresentation } from '@mistriwork/engine/schema/model.js';\nimport type {\n AppCommandChoice,\n AppCommandField,\n AppCommandReference,\n AppFormCommandReference,\n CommandFieldDefault,\n CommandRichTextMentions,\n} from '../../schema/command.js';\nimport { columnKind, tableNamed, type DatabaseSchema, type TableMeta } from '@mistriwork/engine/shared/schema.js';\nimport { ANYONE, parseQuery, quoteValue } from '@mistriwork/engine/shared/query.js';\nimport { maskQuoted } from '@mistriwork/engine/shared/maskQuoted.js';\nimport { parseViewQuery } from '@mistriwork/engine/shared/view.js';\nimport { unknownKey } from '@mistriwork/engine/shared/suggest.js';\nimport { normalizeAppFlagGate } from './appFlagGates.js';\nimport type { FlagCatalog } from './appFlags.js';\nimport { RICH_TEXT_MOST } from '@mistriwork/engine/shared/richText.js';\nimport { mentionLabelColumns } from '@mistriwork/engine/shared/mentionLabel.js';\nimport { commandFieldsDeep, isIsoDate } from '@mistriwork/engine/shared/commandWire.js';\nimport type { DraftRule } from './commandDraft.js';\nexport { BULK_LIMIT, commandFieldsDeep, commandKeyText, isIsoDate } from '@mistriwork/engine/shared/commandWire.js';\n\nexport type {\n Action,\n AppAction,\n AppUpdateAction,\n AppCommandAction,\n AppCommandChoice,\n AppCommandField,\n AppCommandGroup,\n AppCommandLines,\n AppCommandReference,\n AppFormCommandReference,\n AppFlagGate,\n CommandAction,\n CommandField,\n CommandInput,\n CommandInputOf,\n CommandInitialOf,\n} from '../../schema/command.js';\n\n/** A served reference without `targets` names one record. */\nexport const commandTargets = (reference: { targets?: 'one' | 'many' }): 'one' | 'many' => reference.targets ?? 'one';\n\nconst reserved = new Set([...Object.getOwnPropertyNames(Object.prototype), 'prototype']);\nconst identifier = /^[A-Za-z_][A-Za-z0-9_]*$/;\nconst fail = (where: string, reason: string): never => {\n throw new Error(`${where}: ${reason}.`);\n};\n\nfunction record(raw: unknown, keys: readonly string[], where: string): Record<string, unknown> {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw) || ![null, Object.prototype].includes(Object.getPrototypeOf(raw)))\n return fail(where, 'expected a plain data object');\n const result: Record<string, unknown> = Object.create(null);\n for (const key of Reflect.ownKeys(raw)) {\n if (typeof key !== 'string') return fail(where, 'unknown command metadata property');\n if (!keys.includes(key)) throw new Error(`${where} ${unknownKey(key, keys)}.`);\n const descriptor = Object.getOwnPropertyDescriptor(raw, key)!;\n if (!descriptor.enumerable || !('value' in descriptor)) return fail(where, 'expected enumerable data properties');\n result[key] = descriptor.value;\n }\n return result;\n}\n\nfunction text(raw: unknown, where: string, limit = 500): string {\n if (typeof raw !== 'string' || !raw.trim() || raw.length > limit)\n return fail(where, `expected nonempty text of at most ${limit} characters`);\n return raw.trim();\n}\n\n/* A mention's label: a column, or a template of the people's own columns ('{first_name} {last_name}', mentionLabel.ts). */\nfunction mentionLabel(raw: unknown, where: string): string {\n if (typeof raw !== 'string' || !raw.includes('{')) return name(raw, where);\n const read = mentionLabelColumns(raw);\n if ('problem' in read) return fail(where, `label ${read.problem}`);\n for (const column of read.columns) name(column, `${where}, label \"${raw}\"`);\n return raw;\n}\n\nfunction name(raw: unknown, where: string, qualified = false): string {\n if (typeof raw !== 'string' || raw.length > 128) return fail(where, 'expected a logical identifier');\n const parts = raw.split('.');\n if (parts.length > (qualified ? 2 : 1) || parts.some((part) => !identifier.test(part) || reserved.has(part))) {\n return fail(where, 'expected a non-reserved logical identifier');\n }\n return raw;\n}\n\n/**\n * The caps a command's input declaration and its values live under. A list\n * of fields (the command's own, or a group's) holds at most `listFields`; the\n * whole declaration, groups and their members at every depth, at most\n * `totalFields`; a group may hold a group, and no deeper (`depth`). A\n * repeating group admits at most `repeatRows` rows, and on a public form at\n * most `publicRepeatRows`; a public submission's input is at most\n * `publicInputBytes` of JSON, inside the envelope bound every command has.\n */\nexport const COMMAND_INPUT_LIMITS = Object.freeze({\n listFields: 32,\n totalFields: 128,\n depth: 2,\n repeatRows: 50,\n publicRepeatRows: 10,\n choiceOptions: 100,\n publicInputBytes: 32_768,\n});\n\n/* A moment, as ISO 8601 says one: a date, a time to the minute or finer, and a zone — `Z` or an offset. The\n separator may be the `T` or a space, as Postgres writes a timestamptz (`2026-10-12 19:16:00-04`). */\nconst INSTANT = /^(\\d{4}-\\d{2}-\\d{2})[T ]([01]\\d|2[0-3]):([0-5]\\d)(?::([0-5]\\d)(?:\\.(\\d{1,9}))?)?(Z|[+-](?:0\\d|1[0-4])(?::?[0-5]\\d)?)$/;\n\n/**\n * A moment as one spelling: the UTC instant `YYYY-MM-DDTHH:MM:SS.sssZ` it names, or undefined. A value with no zone\n * is not one — the same wall time is a different moment in every zone — and a `Date` is read as the moment it holds.\n * Beyond milliseconds a fraction is dropped, as a JavaScript date keeps it.\n */\nexport function isoInstant(value: unknown): string | undefined {\n let time: number;\n if (value instanceof Date) time = Date.prototype.getTime.call(value);\n else {\n if (typeof value !== 'string' || value.length > 40) return undefined;\n const match = INSTANT.exec(value);\n if (!match || !isIsoDate(match[1])) return undefined;\n const [, date, hour, minute, seconds = '00', fraction = '', zone] = match;\n const digits = zone.replace(':', '');\n const offset = zone === 'Z' ? 0 : (zone[0] === '-' ? -1 : 1) * (Number(digits.slice(1, 3)) * 60 + Number(digits.slice(3, 5) || 0));\n time = Date.parse(`${date}T${hour}:${minute}:${seconds}.${fraction.padEnd(3, '0').slice(0, 3)}Z`) - offset * 60_000;\n }\n if (!Number.isFinite(time)) return undefined;\n const at = new Date(time);\n return at.getUTCFullYear() >= 0 && at.getUTCFullYear() <= 9999 ? at.toISOString() : undefined;\n}\n\n/** Dense, plain list items, each a data property. */\nfunction items(raw: unknown, where: string, what: string, max: number, noun: string): unknown[] {\n if (\n !Array.isArray(raw) ||\n Object.getPrototypeOf(raw) !== Array.prototype ||\n raw.length > max ||\n Reflect.ownKeys(raw).length !== raw.length + 1\n )\n return fail(where, `${what} must be a dense list of at most ${max} ${noun}`);\n const result: unknown[] = [];\n for (let index = 0; index < raw.length; index++) {\n const descriptor = Object.getOwnPropertyDescriptor(raw, String(index));\n if (!descriptor?.enumerable || !('value' in descriptor)) return fail(where, `${what} must contain only enumerable data ${noun}`);\n result.push(descriptor.value);\n }\n return result;\n}\n\n/** The names a field's `when` reads — the input's fields and the record's columns — or a throw where it does not parse. */\ntype ConditionNames = (text: string, where: string) => { fields: string[]; record: string[] };\n\ninterface FieldRules {\n depth: number;\n publicForm: boolean;\n count: { fields: number };\n /* The condition compiler's reading of a `when` (commandDraft.ts draftNames), handed in by parseCommandInput\n (commandInputParse.ts): this module is in every page's module graph, the browser never parses a declaration,\n and a static import here put the compiler on every public page's first paint. */\n names: ConditionNames;\n}\n\nfunction choiceOption(raw: unknown, where: string): AppCommandChoice {\n const o = record(raw, ['value', 'label', 'icon', 'help'], where);\n const value = o.value;\n if (!(typeof value === 'string' && value.trim() && value.length <= 120) && !(typeof value === 'number' && Number.isFinite(value))) {\n return fail(where, 'a choice value must be nonempty text of at most 120 characters or a finite number');\n }\n if (o.icon !== undefined && (typeof o.icon !== 'string' || !/^[a-z][a-z0-9-]{0,63}$/.test(o.icon)))\n return fail(where, \"a choice's icon is an App icon's name, such as 'shield-check'\");\n return {\n value: value as string | number,\n label: text(o.label, where, 120),\n ...(o.icon === undefined ? {} : { icon: o.icon as string }),\n ...(o.help === undefined ? {} : { help: text(o.help, where, 200) }),\n };\n}\n\n/** A field's label when it gives none, read from its key: `applicant_email` is \"Applicant email\", `reviewerId` \"Reviewer id\". */\nexport function fieldLabel(key: string): string {\n const words = key\n .replace(/([a-z0-9])([A-Z])/g, '$1 $2')\n .replace(/_+/g, ' ')\n .trim()\n .toLowerCase();\n return words.charAt(0).toUpperCase() + words.slice(1);\n}\n\n/** A declared pattern, matched whole: what the browser and the server both test a value against. */\nexport const fieldPattern = (source: string): RegExp => new RegExp(`^(?:${source})$`);\n\nconst FORMATS = new Set(['email', 'url', 'phone']);\n\n/** A rich text input's most characters of stored HTML, unless it says otherwise: room for a long note with a table in it. */\nconst RICH_TEXT_FIELD_DEFAULT = 65_536;\n\n/* What a field starts as. A literal, today, now and the record's own value\n are the browser's or prepare's to fill in, so only the shape is judged\n here; whether a literal passes the field's own checks is the server's,\n where the command is defined (businessCommands.ts). */\nconst START_WORDS: Record<string, readonly string[]> = { today: ['date'], now: ['datetime'], me: ['text'] };\nfunction fieldStart(raw: unknown, made: AppCommandField, where: string, rules: FieldRules): CommandFieldDefault {\n const at = `${where}: default`;\n if (made.type === 'group') return fail(where, 'a group starts as its fields do; give each of them its default');\n if (raw && typeof raw === 'object' && !Array.isArray(raw)) {\n const start = raw as Record<string, unknown>;\n const word = Object.keys(start).length === 1 ? (start.auto ?? (typeof start.record === 'string' ? 'record' : undefined)) : undefined;\n if (word === 'record' || word === 'me') {\n if (rules.publicForm) return fail(at, 'a public form has no record and no reader to start a field from');\n if (rules.depth) return fail(at, `${word === 'me' ? 'me' : '{ record }'} fills a field of the input itself, not one inside a group`);\n }\n if (word === 'record') return { record: name(start.record, at) };\n if (typeof word === 'string' && START_WORDS[word]?.includes(made.type)) return { auto: word as 'today' | 'now' | 'me' };\n return fail(at, 'is a literal, today (a date field), now (a datetime field), me (a text field) or { record: column }');\n }\n if (typeof raw === 'string' || typeof raw === 'boolean' || (typeof raw === 'number' && Number.isFinite(raw))) return raw;\n return fail(at, 'is a literal, today (a date field), now (a datetime field), me (a text field) or { record: column }');\n}\n\n/* An input asked only while a condition over the draft holds: a bare name is another field of this input, and\n `record.<column>` the target's own. Its words are read here;\n the names it reads are judged beside its siblings (fieldList) and, for the record's, against the target where\n the command is bound (businessCommandAuthorization.ts), rollups and computed columns included. */\nfunction fieldWhen(f: Record<string, unknown>, where: string, rules: FieldRules): string {\n const at = `${where}: when`;\n if (typeof f.when !== 'string' || !f.when.trim() || f.when.length > 1000)\n return fail(at, \"is a condition over the draft, as text: another field's name (trade_in = true) or record.<column>\");\n if (f.type === 'group') return fail(at, \"is a single field's: a group is asked whole\");\n if (rules.depth) return fail(at, 'asks a field of the input itself, not one inside a group');\n let names: { fields: string[]; record: string[] };\n try {\n names = rules.names(f.when.trim(), at);\n } catch (error) {\n return fail(at, (error as Error).message.replace(/^.*?: /, '').replace(/\\.$/, ''));\n }\n if (names.record.length && rules.publicForm) return fail(at, `reads record.${names.record[0]}, and a public form has no record`);\n return f.when.trim();\n}\n\n/* A mask: `0` a digit, `a` a letter, `*` either, anything else a character drawn as written. It is the value's\n pattern too, over what its places hold. */\nconst MASK = /^[^\\p{Cc}]{1,64}$/u;\nexport function maskPattern(mask: string): string {\n return [...mask].map((c) => (c === '0' ? '\\\\d' : c === 'a' ? '[A-Za-z]' : c === '*' ? '[A-Za-z0-9]' : '')).join('');\n}\n/** How many characters a mask's places hold. */\nconst maskPlaces = (mask: string): number => [...mask].filter((c) => c === '0' || c === 'a' || c === '*').length;\n\nconst KEEPS = ['last4', 'hash', 'none', 'handler'] as const;\nconst PRESENTATIONS: Record<string, readonly string[]> = {\n number: ['slider'],\n money: ['slider'],\n boolean: ['buttons'],\n choice: ['cards', 'buttons', 'select', 'radios'],\n choices: ['cards', 'select'],\n};\nconst LIVE_NAME = /^[A-Za-z_][A-Za-z0-9_]{0,31}$/;\nconst COMPONENT_NAME = /^[A-Za-z][A-Za-z0-9_-]{0,47}$/;\n\n/* A month, `YYYY-MM`. */\nexport const isIsoMonth = (value: unknown): value is string => typeof value === 'string' && /^\\d{4}-(0[1-9]|1[0-2])$/.test(value);\n\n/* Choices read from a table: its rows by key, shown by a column. */\nfunction choiceSource(f: Record<string, unknown>, where: string): { from: string; by: string; labelColumn: string } {\n if (f.options !== undefined) return fail(where, 'names its options or a table to read them from (from), not both');\n if (f.labelColumn === undefined)\n return fail(where, `reads its choices from \"${String(f.from)}\" and needs labelColumn: the column each is shown by`);\n return {\n from: name(f.from, `${where}, from`, true),\n by: f.by === undefined ? 'id' : name(f.by, `${where}, by`),\n labelColumn: name(f.labelColumn, `${where}, labelColumn`),\n };\n}\n\n/* `as`: how the field is drawn, one its kind can be drawn as. */\nfunction presentation(f: Record<string, unknown>, where: string): { as?: CommandFieldPresentation } {\n if (f.as === undefined) return {};\n const allowed = PRESENTATIONS[String(f.type)] ?? [];\n if (typeof f.as !== 'string' || !allowed.includes(f.as))\n return fail(\n where,\n allowed.length\n ? `as is how a ${String(f.type)} field is drawn: ${allowed.map((a) => `'${a}'`).join(', ')}`\n : `a ${String(f.type)} field is drawn one way; it takes no as`,\n );\n return { as: f.as as CommandFieldPresentation };\n}\n\n/* `input`: the App's own input component, by its name in `ui.inputs`. A public form's page draws it too: the code is\n served to a visitor like a public page's components, and the value is still the field's kind's to judge. */\nfunction componentName(raw: unknown, where: string): string {\n if (typeof raw !== 'string' || !COMPONENT_NAME.test(raw))\n return fail(where, \"input names one of the App's input components (ui.inputs) by its name\");\n return raw;\n}\n\n/* `live`: lines said under a number as it is typed — expressions over the input's numbers, read where the siblings\n are known (fieldList), never sent. */\nfunction liveLines(raw: unknown, where: string): Record<string, string> {\n const lines = record(raw, Object.keys(raw && typeof raw === 'object' ? raw : {}), `${where}, live`);\n const entries = Object.entries(lines);\n if (!entries.length || entries.length > 4) return fail(where, \"live says one to four lines: { monthly: 'biweekly_gross * 26 / 12' }\");\n for (const [word, expression] of entries) {\n if (!LIVE_NAME.test(word)) return fail(where, `live \"${word}\" is a short word, said after the figure`);\n if (typeof expression !== 'string' || !expression.trim() || expression.length > 500)\n return fail(where, `live \"${word}\" is an expression over this input's numbers, at most 500 characters`);\n }\n return Object.fromEntries(entries.map(([word, expression]) => [word, (expression as string).trim()]));\n}\n\n/** The input names a live line reads: the identifiers in its expression. */\nconst liveNames = (expression: string): string[] =>\n [...new Set(expression.match(/[A-Za-z_][A-Za-z0-9_]*/g) ?? [])].filter((word) => !/^(?:and|or|not|true|false|null)$/.test(word));\n\nfunction field(key: string, raw: unknown, where: string, rules: FieldRules): AppCommandField {\n const made = fieldShape(key, raw, where, rules);\n const start = (raw as { default?: unknown }).default;\n return start === undefined ? made : ({ ...made, default: fieldStart(start, made, where, rules) } as AppCommandField);\n}\n\nfunction fieldShape(key: string, raw: unknown, where: string, rules: FieldRules): AppCommandField {\n if (++rules.count.fields > COMMAND_INPUT_LIMITS.totalFields) {\n return fail(where, `a command declares at most ${COMMAND_INPUT_LIMITS.totalFields} fields, groups and their members included`);\n }\n /* A reference picker's words before 1.0, refused with the ones that replaced them. */\n for (const [old, now] of [\n ['resource', '\"from\": the resource the picker offers'],\n ['value', '\"by\": the key column a pick stores'],\n ] as const) {\n if (raw && typeof raw === 'object' && Object.hasOwn(raw, old)) throw new Error(`${where} has \"${old}\", which is now ${now}.`);\n }\n const f = record(\n raw,\n [\n 'type',\n 'label',\n 'help',\n 'optional',\n 'min',\n 'max',\n 'format',\n 'pattern',\n 'message',\n 'case',\n 'integer',\n 'step',\n 'below',\n 'above',\n 'by',\n 'labelColumn',\n 'query',\n 'options',\n 'fields',\n 'repeat',\n 'keyed',\n 'accept',\n 'description',\n 'mentions',\n 'images',\n 'default',\n 'when',\n 'from',\n 'whose',\n 'show',\n 'copy',\n 'mask',\n 'sensitive',\n 'live',\n 'minor',\n 'currency',\n 'statement',\n 'exclusive',\n 'as',\n 'input',\n ],\n where,\n );\n if (f.keyed !== undefined)\n throw new Error(`${where} has \"keyed\", which is now sensitive: { keep: 'hash' }: one way to keep a value the App must not hold.`);\n if (f.optional !== undefined && typeof f.optional !== 'boolean') return fail(where, 'optional must be true or false');\n // Required unless it says otherwise: a form asks for what its command needs.\n // A group says its words as `description`, as a form's groups do; `help` stays under a single input.\n if (f.type === 'group' && f.help !== undefined)\n throw new Error(`${where} (a group field) ${unknownKey('help', ['type', 'label', 'optional', 'fields', 'repeat', 'description'])}.`);\n if (f.type === 'lines' && f.help !== undefined)\n throw new Error(\n `${where} (a lines field) ${unknownKey('help', ['type', 'label', 'from', 'show', 'copy', 'query', 'min', 'max', 'description'])}.`,\n );\n const base = {\n name: key,\n label: f.label === undefined ? fieldLabel(key) : text(f.label, where, 120),\n ...(f.help === undefined ? {} : { help: text(f.help, where) }),\n ...((f.type === 'group' || f.type === 'lines') && f.description !== undefined ? { help: text(f.description, where) } : {}),\n ...(f.optional === true ? {} : { required: true }),\n ...(f.when === undefined ? {} : { when: fieldWhen(f, where, rules) }),\n ...presentation(f, where),\n ...(f.input === undefined ? {} : { input: componentName(f.input, where) }),\n };\n const common = ['type', 'label', 'help', 'optional', 'default', 'when', 'as', 'input'];\n const extra = Object.keys(f).filter((key) => !common.includes(key));\n const only = (allowed: string[]) => {\n const stray = extra.find((key) => !allowed.includes(key));\n if (stray !== undefined) throw new Error(`${where} (a ${String(f.type)} field) ${unknownKey(stray, [...common, ...allowed])}.`);\n };\n const sibling = (key: 'below' | 'above') => (f[key] === undefined ? {} : { [key]: name(f[key], where) });\n if (f.type === 'text' || f.type === 'textarea') {\n only(['min', 'max', 'format', 'pattern', 'message', 'case', 'mask', 'sensitive']);\n if (f.mask !== undefined) {\n if (f.type !== 'text' || typeof f.mask !== 'string' || !MASK.test(f.mask) || !maskPlaces(f.mask))\n return fail(\n where,\n \"mask is the shape a text's value is typed in: 0 a digit, a a letter, * either ('000-00-0000'), on a text field\",\n );\n if (f.pattern !== undefined || f.format !== undefined)\n return fail(where, 'a mask is its own pattern; give a mask, a pattern or a format');\n }\n let sensitive: { keep: (typeof KEEPS)[number] } | undefined;\n if (f.sensitive !== undefined) {\n const said = record(f.sensitive, ['keep'], `${where}, sensitive`);\n if (f.type !== 'text' || !(KEEPS as readonly unknown[]).includes(said.keep))\n return fail(where, \"sensitive is { keep: 'last4' | 'hash' | 'none' | 'handler' }, on a text field\");\n if (f.default !== undefined) return fail(where, 'a sensitive field is never prefilled; it takes no default');\n if (said.keep === 'handler' && rules.depth)\n return fail(where, \"keep: 'handler' is a field of the input itself, not one inside a group\");\n sensitive = { keep: said.keep as (typeof KEEPS)[number] };\n }\n for (const bound of ['min', 'max'] as const) {\n if (f[bound] !== undefined && (!Number.isSafeInteger(f[bound]) || Number(f[bound]) < 1 || Number(f[bound]) > 16384)) {\n return fail(where, `${bound} must be a number of characters from 1 to 16384`);\n }\n }\n if (f.min !== undefined && f.max !== undefined && Number(f.min) > Number(f.max)) return fail(where, 'min must not exceed max');\n if (f.format !== undefined && (f.type !== 'text' || !FORMATS.has(f.format as string)))\n return fail(where, 'format is \"email\", \"url\" or \"phone\", on a text field');\n if (f.pattern !== undefined) {\n if (typeof f.pattern !== 'string' || !f.pattern || f.pattern.length > 500)\n return fail(where, \"pattern must be a regular expression's source, at most 500 characters\");\n try {\n fieldPattern(f.pattern);\n } catch {\n return fail(where, 'pattern is not a regular expression');\n }\n }\n // A mask is a pattern too, and its sentence is the author's to word (\"Enter the 5 digits of your ZIP\").\n if (f.message !== undefined && f.pattern === undefined && f.format === undefined && f.mask === undefined)\n return fail(where, 'message is what a pattern, format or mask says; give one');\n if (f.case !== undefined && f.case !== 'upper' && f.case !== 'lower') return fail(where, 'case is \"upper\" or \"lower\"');\n return {\n ...base,\n type: f.type,\n ...(f.min === undefined ? {} : { minLength: f.min as number }),\n ...(f.max === undefined ? {} : { maxLength: f.max as number }),\n ...(f.format === undefined ? {} : { format: f.format as 'email' | 'url' | 'phone' }),\n ...(f.pattern === undefined ? {} : { pattern: f.pattern as string }),\n ...(f.message === undefined ? {} : { message: text(f.message, where, 200) }),\n ...(f.case === undefined ? {} : { case: f.case as 'upper' | 'lower' }),\n ...(f.mask === undefined ? {} : { mask: f.mask as string }),\n ...(sensitive ? { sensitive } : {}),\n };\n }\n if (f.type === 'richText') {\n only(['min', 'max', 'mentions', 'images']);\n if (rules.publicForm && (f.mentions !== undefined || f.images !== undefined))\n return fail(where, `input \"${base.name}\" offers mentions or images; a public form may not read a directory or keep files`);\n if (f.max !== undefined && (!Number.isSafeInteger(f.max) || Number(f.max) < 1 || Number(f.max) > RICH_TEXT_MOST))\n return fail(where, `max must be a number of characters from 1 to ${RICH_TEXT_MOST}`);\n // The words a reader sees, not the markup kept: a body of 20 characters is 20 characters of text.\n if (f.min !== undefined && (!Number.isSafeInteger(f.min) || Number(f.min) < 1 || Number(f.min) > 16384))\n return fail(where, 'min must be a number of characters of text from 1 to 16384');\n let mentions: CommandRichTextMentions | undefined;\n if (f.mentions !== undefined) {\n for (const said of [f.mentions, (f.mentions as Record<string, unknown> | null)?.groups])\n if (said && typeof said === 'object' && Object.hasOwn(said, 'label'))\n throw new Error(`${where}, mentions has \"label\", which is now \"labelColumn\": the column a person is shown by.`);\n const m = record(f.mentions, ['from', 'by', 'labelColumn', 'groups'], `${where}, mentions`);\n /* The teams an @ names as well, as a display rule's `mentions.groups`: a command's\n reply or work note reached only people, though the conversation draws and tells a team. */\n const g =\n m.groups === undefined ? undefined : record(m.groups, ['from', 'by', 'labelColumn', 'through'], `${where}, mentions.groups`);\n mentions = {\n from: name(m.from, `${where}, mentions`, true),\n by: name(m.by, `${where}, mentions`),\n ...(m.labelColumn === undefined ? {} : { labelColumn: mentionLabel(m.labelColumn, `${where}, mentions`) }),\n ...(g\n ? {\n groups: {\n from: name(g.from, `${where}, mentions.groups`, true),\n by: name(g.by, `${where}, mentions.groups`),\n ...(g.labelColumn === undefined ? {} : { labelColumn: mentionLabel(g.labelColumn, `${where}, mentions.groups`) }),\n /* The link rows that put a person in a team, as a display rule's: a team no row\n names has nobody to tell, so the editor does not offer it. */\n ...(g.through === undefined ? {} : { through: name(g.through, `${where}, mentions.groups`, true) }),\n },\n }\n : {}),\n };\n }\n return {\n ...base,\n type: 'richText',\n ...(f.min === undefined ? {} : { minLength: f.min as number }),\n maxLength: (f.max as number | undefined) ?? RICH_TEXT_FIELD_DEFAULT,\n ...(mentions ? { mentions } : {}),\n ...(f.images === undefined ? {} : { images: name(f.images, `${where}, images`, true) }),\n };\n }\n if (f.type === 'number' || f.type === 'money') {\n only(\n f.type === 'money'\n ? ['min', 'max', 'step', 'minor', 'currency', 'below', 'above', 'live']\n : ['min', 'max', 'integer', 'step', 'below', 'above', 'live'],\n );\n if (f.minor !== undefined && typeof f.minor !== 'boolean') return fail(where, 'minor must be boolean');\n if (f.currency !== undefined && (typeof f.currency !== 'string' || !/^[A-Z]{3}$/.test(f.currency)))\n return fail(where, \"currency must be an ISO 4217 code such as 'EUR'\");\n if (f.minor === true && f.step !== undefined)\n return fail(where, 'a minor-unit amount takes its precision from the currency, not a step');\n if (f.type === 'money' && f.step === undefined && f.minor !== true) f.step = 0.01;\n if (f.type === 'money' && f.min === undefined) f.min = 0;\n for (const key of ['min', 'max'] as const)\n if (f[key] !== undefined && (typeof f[key] !== 'number' || !Number.isFinite(f[key]))) fail(where, `${key} must be finite`);\n if (f.min !== undefined && f.max !== undefined && Number(f.min) > Number(f.max)) return fail(where, 'min must not exceed max');\n if (f.integer !== undefined && typeof f.integer !== 'boolean') return fail(where, 'integer must be boolean');\n if (\n f.step !== undefined &&\n (typeof f.step !== 'number' ||\n !Number.isFinite(f.step) ||\n f.step <= 0 ||\n f.step > 1e9 ||\n (f.integer === true && !Number.isSafeInteger(f.step)))\n )\n return fail(where, 'step must be a positive number, whole on an integer field');\n if (base.as === 'slider' && (f.min === undefined || f.max === undefined))\n return fail(where, \"a slider runs between its ends: give min and max, or leave out as: 'slider'\");\n return {\n ...base,\n type: f.type,\n ...(f.min === undefined ? {} : { min: f.min as number }),\n ...(f.max === undefined ? {} : { max: f.max as number }),\n ...(f.integer === undefined ? {} : { integer: f.integer as boolean }),\n ...(f.step === undefined ? {} : { step: f.step as number }),\n ...(f.minor === true ? { minor: true } : {}),\n ...(f.currency === undefined ? {} : { currency: f.currency as string }),\n ...sibling('below'),\n ...sibling('above'),\n ...(f.live === undefined ? {} : { live: liveLines(f.live, where) }),\n } as AppCommandField;\n }\n if (f.type === 'boolean') {\n only(['accept', 'statement']);\n if (f.accept !== undefined && f.accept !== true) return fail(where, 'accept must be true when it is given');\n // A box that must be checked is never left out: optional would make it a yes or a no again.\n if (f.accept === true && f.optional === true) return fail(where, 'a field that must be accepted cannot also be optional');\n // A statement is the words a checkbox stands beside, whether it must be checked or may be left: a consent to texts\n // is as long as the one to a credit check. A yes or a no drawn as two buttons has no box to stand beside.\n if (f.statement !== undefined && base.as === 'buttons')\n return fail(where, \"statement is the words a checkbox stands beside; as: 'buttons' asks a yes or a no, and has none\");\n if (f.accept === true && base.as === 'buttons')\n return fail(where, \"a box that must be checked is a checkbox; as: 'buttons' asks a yes or a no\");\n return {\n ...base,\n type: 'boolean',\n ...(f.accept === true ? { accept: true as const } : {}),\n ...(f.statement === undefined ? {} : { statement: text(f.statement, `${where}, statement`, 2000) }),\n };\n }\n if (f.type === 'month') {\n only(['min', 'max', 'below', 'above']);\n for (const key of ['min', 'max'] as const)\n if (f[key] !== undefined && !isIsoMonth(f[key])) fail(where, `${key} must be a month, YYYY-MM`);\n if (f.min !== undefined && f.max !== undefined && String(f.min) > String(f.max)) return fail(where, 'min must not be after max');\n return {\n ...base,\n type: 'month',\n ...(f.min === undefined ? {} : { min: f.min as string }),\n ...(f.max === undefined ? {} : { max: f.max as string }),\n ...sibling('below'),\n ...sibling('above'),\n };\n }\n if (f.type === 'date') {\n only(['min', 'max', 'below', 'above']);\n for (const key of ['min', 'max'] as const)\n if (f[key] !== undefined && !isIsoDate(f[key])) fail(where, `${key} must be an ISO date, YYYY-MM-DD`);\n if (f.min !== undefined && f.max !== undefined && String(f.min) > String(f.max)) return fail(where, 'min must not be after max');\n return {\n ...base,\n type: 'date',\n ...(f.min === undefined ? {} : { min: f.min as string }),\n ...(f.max === undefined ? {} : { max: f.max as string }),\n ...sibling('below'),\n ...sibling('above'),\n };\n }\n /* A moment: what a person types as a date and a wall time, in the App's time zone, arrives as the UTC instant. */\n if (f.type === 'datetime') {\n only(['min', 'max', 'below', 'above']);\n const bound = (key: 'min' | 'max') => {\n if (f[key] === undefined) return {};\n const at = isoInstant(f[key]);\n return at === undefined || typeof f[key] !== 'string'\n ? fail(where, `${key} must be an ISO 8601 date and time with its zone, such as 2026-10-12T09:00:00Z`)\n : { [key]: at };\n };\n const min = bound('min'),\n max = bound('max');\n if (min.min !== undefined && max.max !== undefined && min.min > max.max) return fail(where, 'min must not be after max');\n return { ...base, type: 'datetime', ...min, ...max, ...sibling('below'), ...sibling('above') };\n }\n if (f.type === 'choice' || f.type === 'choices') {\n only(\n f.type === 'choices' ? ['options', 'from', 'by', 'labelColumn', 'min', 'max', 'exclusive'] : ['options', 'from', 'by', 'labelColumn'],\n );\n const source = f.from === undefined ? undefined : choiceSource(f, where);\n if (!source && (f.by !== undefined || f.labelColumn !== undefined))\n return fail(where, 'by and labelColumn read choices from a table: give from');\n const options = source\n ? []\n : items(f.options, where, 'options', COMMAND_INPUT_LIMITS.choiceOptions, 'options').map((option, index) =>\n choiceOption(option, `${where}, option ${index + 1}`),\n );\n if (!source && !options.length) return fail(where, 'a choice needs at least one option');\n if (new Set(options.map((option) => `${typeof option.value}:${option.value}`)).size !== options.length)\n return fail(where, 'duplicate choice value');\n if (f.type === 'choice') return { ...base, type: 'choice', options, ...(source ? { source } : {}) };\n const cap = COMMAND_INPUT_LIMITS.choiceOptions;\n for (const bound of ['min', 'max'] as const)\n if (\n f[bound] !== undefined &&\n (!Number.isSafeInteger(f[bound]) || Number(f[bound]) < (bound === 'max' ? 1 : 0) || Number(f[bound]) > cap)\n )\n return fail(where, `${bound} must be a whole number of choices up to ${cap}`);\n if (f.min !== undefined && f.max !== undefined && Number(f.min) > Number(f.max)) return fail(where, 'min must not exceed max');\n if (f.min !== undefined && Number(f.min) > 0 && f.optional === true)\n return fail(where, 'a field that must have a choice (min) is not optional');\n let exclusive: (string | number)[] | undefined;\n if (f.exclusive !== undefined) {\n exclusive = items(f.exclusive, `${where}, exclusive`, 'exclusive', cap, 'values') as (string | number)[];\n if (!exclusive.length) return fail(where, 'exclusive names one or more of the options that stand alone');\n if (!source && exclusive.some((value) => !options.some((option) => option.value === value)))\n return fail(where, 'exclusive names a value that is not one of the options');\n }\n return {\n ...base,\n type: 'choices',\n options,\n ...(source ? { source } : {}),\n ...(f.min === undefined ? {} : { min: f.min as number }),\n ...(f.max === undefined ? {} : { max: f.max as number }),\n ...(exclusive ? { exclusive } : {}),\n };\n }\n /* On a public form too: its choices are what `auth.public` opens, read signed out, and the App's public grant is\n judged against it where both are known (forms/savedFormChecks.ts, [public-form-picker]). */\n if (f.type === 'reference') {\n only(['from', 'by', 'labelColumn', 'query']);\n return {\n ...base,\n type: 'reference',\n from: name(f.from, where, true),\n by: name(f.by, where),\n labelColumn: name(f.labelColumn, where),\n ...(f.query === undefined ? {} : { query: text(f.query, where, 2000) }),\n };\n }\n /* Answers to questions that are rows: the rows of `from` an administrator edits while the App runs,\n those `whose` names the record by and those that name none. One object of answers, judged against those rows\n where the command runs; the form draws them once prepare has read them. A public form has no reader to read\n them for, and a group's rows would each ask the same questions again. */\n if (f.type === 'defined') {\n only(['from', 'whose']);\n if (rules.publicForm) return fail(where, `input \"${base.name}\" asks questions defined in rows; a public form may not read them`);\n if (rules.depth) return fail(where, 'questions defined in rows are a field of the input itself, not one inside a group');\n // Its rows say what each answer starts as and whether it is asked; the field itself is always one object.\n if (f.default !== undefined || f.when !== undefined)\n return fail(where, 'questions defined in rows take no default and no when; their rows say what is asked');\n return {\n ...base,\n type: 'defined',\n from: name(f.from, `${where}, from`, true),\n ...(f.whose === undefined ? {} : { whose: name(f.whose, `${where}, whose`) }),\n };\n }\n /* A record's lines, as the lines grid asks them (CommandLinesField): rows of a resource the command writes, each\n the columns it shows, read as their kinds where the command runs. The grid reads the catalog and offers pickers,\n which a public form has neither of; and a line is already a row, so it sits in no group. */\n if (f.type === 'lines') {\n only(['from', 'show', 'copy', 'query', 'min', 'max', 'description']);\n if (f.optional !== undefined || f.default !== undefined || f.when !== undefined)\n return fail(where, 'lines take no optional, default or when; a min of 0 lets the form send none');\n if (rules.publicForm) return fail(where, `input \"${base.name}\" asks lines; a public form may not read a directory`);\n if (rules.depth) return fail(where, 'lines are a field of the input itself, not one inside a group');\n const show = items(f.show, `${where}, show`, 'show', 12, 'columns').map((column) => name(column, `${where}, show`));\n if (!show.length || new Set(show).size !== show.length) return fail(where, \"show names one to 12 of the row's columns, each once\");\n let copy: Record<string, string> | undefined;\n if (f.copy !== undefined) {\n const c = f.copy;\n if (!c || typeof c !== 'object' || Array.isArray(c) || ![null, Object.prototype].includes(Object.getPrototypeOf(c)))\n return fail(where, \"copy is an object of a line's column → the reference it is copied through and that record's column\");\n const entries = Object.entries(c);\n if (!entries.length || entries.length > 8) return fail(where, 'copy fills one to 8 columns');\n copy = {};\n for (const [column, path] of entries) {\n name(column, `${where}, copy`);\n const [via, read, more] = typeof path === 'string' ? path.split('.') : [];\n if (more !== undefined || !via || !read || !identifier.test(via) || !identifier.test(read))\n return fail(\n `${where}, copy \"${column}\"`,\n \"is a reference the line shows and the column of the record it names: 'variant_id.price_amount'\",\n );\n if (!show.includes(via)) return fail(`${where}, copy \"${column}\"`, `is copied through \"${via}\", which the line does not show`);\n if (show.includes(column)) return fail(`${where}, copy \"${column}\"`, 'is asked in show; a copied column is filled, not typed');\n copy[column] = path as string;\n }\n }\n let query: Record<string, string> | undefined;\n if (f.query !== undefined) {\n const q = f.query;\n if (!q || typeof q !== 'object' || Array.isArray(q) || ![null, Object.prototype].includes(Object.getPrototypeOf(q)))\n return fail(where, \"query is an object of a shown reference → the rows its picker offers: { variant_id: 'active = true' }\");\n const entries = Object.entries(q);\n if (!entries.length) return fail(where, 'query narrows one or more of the shown references');\n query = {};\n for (const [column, condition] of entries) {\n if (!show.includes(column)) return fail(`${where}, query \"${column}\"`, 'names a column the line does not show');\n const said = text(condition, `${where}, query \"${column}\"`, 2000);\n /* A line has no one record to read: the form's lines are asked before there is one. */\n if (recordColumnsIn(said).length)\n return fail(`${where}, query \"${column}\"`, 'reads record.<column>; a line has no one record to read it from');\n query[column] = said;\n }\n }\n const cap = COMMAND_INPUT_LIMITS.repeatRows;\n for (const bound of ['min', 'max'] as const)\n if (\n f[bound] !== undefined &&\n (!Number.isSafeInteger(f[bound]) || Number(f[bound]) < (bound === 'max' ? 1 : 0) || Number(f[bound]) > cap)\n )\n return fail(where, `${bound} must be a whole number of lines up to ${cap}`);\n if (f.min !== undefined && f.max !== undefined && Number(f.min) > Number(f.max)) return fail(where, 'min must not exceed max');\n return {\n name: base.name,\n label: base.label,\n ...(base.help === undefined ? {} : { help: base.help }),\n type: 'lines',\n from: name(f.from, where, true),\n show,\n ...(copy ? { copy } : {}),\n ...(query ? { query } : {}),\n ...(f.min === undefined ? {} : { min: f.min as number }),\n max: (f.max as number | undefined) ?? cap,\n };\n }\n if (f.type === 'group') {\n only(['fields', 'repeat', 'description']);\n // A group is always present — rows, or an object — so what may be absent is its members.\n if (f.optional !== undefined) return fail(where, 'a group is never optional; its fields are, or its repeat has no min');\n if (rules.depth >= COMMAND_INPUT_LIMITS.depth) return fail(where, `groups nest at most ${COMMAND_INPUT_LIMITS.depth} deep`);\n let repeat: { min?: number; max: number } | undefined;\n if (f.repeat !== undefined) {\n const r = record(f.repeat, ['min', 'max'], `${where}, repeat`);\n const cap = rules.publicForm ? COMMAND_INPUT_LIMITS.publicRepeatRows : COMMAND_INPUT_LIMITS.repeatRows;\n if (!Number.isSafeInteger(r.max) || Number(r.max) < 1 || Number(r.max) > cap) {\n return fail(where, `repeat max must be an integer from 1 to ${cap}${rules.publicForm ? ' on a public form' : ''}`);\n }\n if (r.min !== undefined && (!Number.isSafeInteger(r.min) || Number(r.min) < 0 || Number(r.min) > Number(r.max))) {\n return fail(where, 'repeat min must be an integer from 0 to its max');\n }\n repeat = { ...(r.min === undefined ? {} : { min: r.min as number }), max: r.max as number };\n }\n const fields = fieldList(f.fields, where, { ...rules, depth: rules.depth + 1 });\n if (!fields.length) return fail(where, 'a group needs at least one field');\n return {\n name: base.name,\n label: base.label,\n ...(base.help === undefined ? {} : { help: base.help }),\n type: 'group',\n fields,\n ...(repeat ? { repeat } : {}),\n };\n }\n return fail(where, 'unknown command input type');\n}\n\n/* Fields keyed by name, in the order written: the object's own keys, each a\n plain data property. A field below or above another names a sibling of\n its own kind, beside it in this list. */\nfunction fieldList(raw: unknown, where: string, rules: FieldRules): AppCommandField[] {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw) || ![null, Object.prototype].includes(Object.getPrototypeOf(raw))) {\n return fail(where, 'input must be an object of fields keyed by name');\n }\n const keys = Reflect.ownKeys(raw);\n if (keys.length > COMMAND_INPUT_LIMITS.listFields) return fail(where, `input holds at most ${COMMAND_INPUT_LIMITS.listFields} fields`);\n const list = keys.map((key) => {\n const descriptor = typeof key === 'string' ? Object.getOwnPropertyDescriptor(raw, key) : undefined;\n if (typeof key !== 'string' || !descriptor?.enumerable || !('value' in descriptor))\n return fail(where, 'input must contain only enumerable data fields');\n return field(name(key, where), descriptor.value, `${where}, input \"${key}\"`, rules);\n });\n const byName = new Map(list.map((item) => [item.name, item]));\n const sensitiveField = (named: string) => {\n const other = byName.get(named);\n return other?.type === 'text' && other.sensitive !== undefined;\n };\n for (const item of list) {\n if (item.type !== 'group' && item.type !== 'lines' && item.when !== undefined) {\n for (const named of rules.names(item.when, where).fields) {\n const other = byName.get(named);\n if (other === item) return fail(where, `input \"${item.name}\" is asked when \"${named}\"…, which is itself`);\n if (!other) {\n /* Before rc.1 a bare name was the record's column; now it is a field of the input. Said with the rewrite. */\n const unknown = rules.names(item.when, where).fields.filter((word) => !byName.has(word));\n const rewrite = unknown.reduce(\n (text, word) => text.replace(new RegExp(`(?<![\\\\w.'\"])${word}(?![\\\\w'\"])`, 'g'), `record.${word}`),\n item.when,\n );\n return fail(\n where,\n `input \"${item.name}\" is asked when \"${named}\"…, which is not a field of this input. A bare name in a field's when is ` +\n `another field of the input; the record's own column is record.${named}: when: ${JSON.stringify(rewrite)}`,\n );\n }\n if (sensitiveField(named))\n return fail(where, `input \"${item.name}\" is asked when \"${named}\"…, a sensitive field; nothing reads a sensitive value`);\n if (\n other.type === 'group' ||\n other.type === 'lines' ||\n other.type === 'richText' ||\n other.type === 'choices' ||\n other.type === 'defined'\n )\n return fail(where, `input \"${item.name}\" is asked when \"${named}\"…, a ${other.type} field a condition cannot compare`);\n }\n }\n if ((item.type === 'number' || item.type === 'money') && item.live) {\n for (const [word, expression] of Object.entries(item.live)) {\n for (const named of liveNames(expression)) {\n const other = byName.get(named);\n if (!other || (other.type !== 'number' && other.type !== 'money'))\n return fail(where, `input \"${item.name}\" live \"${word}\" reads \"${named}\", which is not a number field of this input`);\n }\n }\n }\n for (const rule of ['below', 'above'] as const) {\n const other = (item as { below?: string; above?: string })[rule];\n if (other === undefined) continue;\n const sibling = list.find((candidate) => candidate.name === other);\n if (\n !sibling ||\n sibling === item ||\n (sibling.type !== item.type && !(['number', 'money'].includes(sibling.type) && ['number', 'money'].includes(item.type)))\n ) {\n return fail(where, `input \"${item.name}\" is ${rule} \"${other}\", which must be another ${item.type} field beside it`);\n }\n }\n }\n return list;\n}\n\n/** The field parser behind parseCommandInput (commandInputParse.ts), which hands it the condition compiler's reading. */\nexport function parseCommandFields(\n raw: unknown,\n where: string,\n options: { publicForm?: boolean; names: ConditionNames },\n): AppCommandField[] {\n return fieldList(raw, where, { depth: 0, publicForm: options.publicForm === true, count: { fields: 0 }, names: options.names });\n}\n\n/**\n * What the server knows of a command, for an App that names it: its\n * registration's contract, as the browser is served it. The App writes the\n * name alone (`command: 'assignReviewer'`); the definition lives in the\n * server-only commands module, and loading the App fills each name in from\n * here — so an input is declared once, where the handler reads it.\n */\nexport interface CommandCatalogEntry {\n readonly enabledBy?: import('../../schema/command.js').AppFlagGate;\n /** The contract's version: authority.lock's, 1 until a breaking change is bumped. */\n readonly version: number;\n /** The logical resource it acts on, or inserts into when it names no record. */\n readonly target: string;\n /** How many existing records a request names: one, a selection, or none (a form's command). */\n readonly targets: 'one' | 'many' | 'none';\n readonly realm?: 'tenant' | 'platform';\n /** Run by a public form, and by nothing else. */\n readonly public?: true;\n /** What must hold of the record, in the query language: the command's own `when`, which its actions carry. */\n readonly when?: string;\n readonly input: readonly AppCommandField[];\n /** Its rules over the draft (`rules`), which a form checks step by step and the server again as it runs. */\n readonly rules?: readonly DraftRule[];\n /** A form's command whose form prepares its starting values (`initial`, or `default: me`). */\n readonly prepares?: true;\n}\nexport type CommandCatalog = ReadonlyMap<string, CommandCatalogEntry>;\n\n/* The catalog the App being parsed names its commands from. Parsing is\n synchronous and one App at a time, so the loader sets it around the parse\n (withCommandCatalog) rather than threading it through every block's parser. */\nlet catalog: CommandCatalog | undefined;\n\n/** Whether a word names one of the App's commands, in the catalog the App is being parsed with. */\nexport const isRegisteredCommand = (name: string): boolean => Boolean(catalog?.has(name));\n\n/** A registered command's catalog entry, or undefined for a name the catalog lacks. */\nexport const registeredCommand = (name: string): CommandCatalogEntry | undefined => catalog?.get(name);\n\n/** Check every registered command, including commands not drawn in this App. */\nexport function checkCommandCatalogFlagGates(flags: FlagCatalog): void {\n for (const entry of catalog?.values() ?? []) {\n const gate = commandGate(entry);\n if (gate.enabledBy) normalizeAppFlagGate(gate.enabledBy, flags);\n }\n}\n\nfunction commandGate(entry: CommandCatalogEntry): Pick<AppCommandReference, 'enabledBy'> {\n if (!entry || typeof entry !== 'object' || ![Object.prototype, null].includes(Object.getPrototypeOf(entry)))\n throw new Error('Command catalog entries require plain data.');\n const descriptor = Object.getOwnPropertyDescriptor(entry, 'enabledBy');\n if (!descriptor) return {};\n if (!descriptor.enumerable || !('value' in descriptor)) throw new Error('Command enabledBy must be an own data property.');\n return { enabledBy: normalizeAppFlagGate(descriptor.value) };\n}\n\n/** Run `parse` with the App's commands to resolve names from; a name met with none loaded is refused, naming the flag. */\nexport function withCommandCatalog<T>(commands: CommandCatalog | undefined, parse: () => T): T {\n const before = catalog;\n catalog = commands;\n try {\n return parse();\n } finally {\n catalog = before;\n }\n}\n\n/** The catalog entry an authored name stands for, or the refusal that says why there is none. */\nfunction registered(raw: unknown, where: string): [string, CommandCatalogEntry] {\n if (typeof raw !== 'string') {\n return fail(where, \"names its command by name, as the commands module registers it: command: 'assignReviewer'\");\n }\n if (!catalog) {\n return fail(\n where,\n `names the command \"${raw}\", and no commands module is loaded. Give the App its commands module with --commands <file>, ` +\n \"or keep it as commands.ts beside the App's manifest\",\n );\n }\n const entry = catalog.get(raw);\n if (!entry) {\n const known = [...catalog.keys()];\n return fail(\n where,\n `names the command \"${raw}\", which the commands module does not register${known.length ? ` (${unknownKey(raw, known).replace(/^has no \"[^\"]*\"\\.\\s*/, '')})` : ''}`,\n );\n }\n return [raw, entry];\n}\n\nconst served = (input: readonly AppCommandField[]): AppCommandField[] => structuredClone(input) as AppCommandField[];\n\n/** A command an action or an import runs on an existing record: its name, resolved to what the browser is served. */\nexport function parseCommandReference(raw: unknown, where: string): AppCommandReference {\n const [id, entry] = registered(raw, where);\n if (entry.targets === 'none')\n return fail(where, `names \"${id}\", which names no existing record (it is a form's command); run it from commandForm`);\n return {\n id,\n version: entry.version,\n target: entry.target,\n ...(entry.targets === 'many' ? { targets: 'many' as const } : {}),\n ...(entry.realm === undefined ? {} : { realm: entry.realm }),\n ...commandGate(entry),\n input: served(entry.input),\n ...(entry.rules?.length ? { rules: structuredClone(entry.rules) as DraftRule[] } : {}),\n };\n}\n\n/** A command an API exposes, by name: one on a record, or a signed-in form's, as its registration says. */\nexport function parseExposedCommandReference(raw: unknown, where: string): AppCommandReference | AppFormCommandReference {\n const [, entry] = registered(raw, where);\n return entry.targets === 'none' ? parseFormCommandReference(raw, where, false) : parseCommandReference(raw, where);\n}\n\n/**\n * The command a form runs, by name: one that names no existing record. A\n * public form's must be registered `public: true`, and a signed-in form's\n * must not: a public command runs only for whoever submits a public form.\n */\nexport function parseFormCommandReference(raw: unknown, where: string, publicForm: boolean): AppFormCommandReference {\n const [id, entry] = registered(raw, where);\n if (entry.targets !== 'none')\n return fail(\n where,\n `names \"${id}\", which acts on an existing record; a form's command creates records and names none (targets: 'none')`,\n );\n if (publicForm && entry.public !== true) return fail(where, `is public and names \"${id}\", which is not registered with public: true`);\n if (!publicForm && entry.public === true) {\n return fail(\n where,\n `is signed in and names \"${id}\", which is registered with public: true. A public command runs only from a public form (public: true)`,\n );\n }\n return {\n id,\n version: entry.version,\n ...commandGate(entry),\n input: served(entry.input),\n ...(entry.rules?.length ? { rules: structuredClone(entry.rules) as DraftRule[] } : {}),\n ...(entry.prepares ? { prepares: true } : {}),\n };\n}\n\n/** Schema authoring checks; command registration and permission checks are server-owned. */\nexport function commandReferenceProblem(command: AppCommandReference, owner: string, schema: DatabaseSchema): string | undefined {\n const target = tableNamed(schema, command.target);\n if (!target || target.id !== owner) return 'Command target must match the action’s owning resource.';\n if (target.isView || !target.columns.some((column) => column.primaryKey)) return 'Command target must be a table with a primary key.';\n return commandInputProblem(command.input, schema, { table: target, many: command.targets === 'many' });\n}\n\n/* `record.<column>` in a picker's query: the column of the record the command\n acts on, the word a timeline's `side` already reads it by. */\nconst RECORD_WORD = /(?<![\\w$.])record\\.([A-Za-z_]\\w*)/g;\n\n/** The record's columns a reference input's query names (`sales_order_id = record.id`). */\nexport const recordColumnsIn = (query: string): string[] => [...maskQuoted(query).matchAll(RECORD_WORD)].map((m) => m[1]);\n\n/**\n * A picker's query with the record's values in place of its `record.<column>`\n * words, as prepare serves it: null when one of them is empty (or not the\n * reader's to see), which offers nothing rather than every row.\n */\nexport function boundRecordQuery(query: string, row: Readonly<Record<string, unknown>>): string | null {\n let out = query;\n for (const m of [...maskQuoted(query).matchAll(RECORD_WORD)].reverse()) {\n const value = row[m[1]];\n if (value === null || value === undefined) return null;\n out = `${out.slice(0, m.index)}${quoteValue(value)}${out.slice(m.index + m[0].length)}`;\n }\n return out;\n}\n\n/* A value of the column's kind, so a query that names the record is judged as it will read once bound. */\nconst SAMPLE = { number: 1, boolean: true, date: '2000-01-01', text: 'x', other: 'x' } as const;\n\n/* Questions defined in rows: rows with the columns a row-defined field's have, and `whose` a column of\n them that holds the record's one key. */\nfunction definedInputProblem(\n f: Extract<AppCommandField, { type: 'defined' }>,\n schema: DatabaseSchema,\n target: { table: TableMeta; many: boolean } | undefined,\n): string | undefined {\n const from = tableNamed(schema, f.from);\n if (!from) return `Command input \"${f.name}\" asks the questions in \"${f.from}\", which is not a resource the command reads.`;\n const missing = ['key', 'label', 'type'].filter((name) => !from.columns.some((column) => column.name === name));\n if (missing.length)\n return `Command input \"${f.name}\": \"${f.from}\" has no ${missing.map((name) => `\"${name}\"`).join(', ')}; a question row has key, label and type (and may have options, required, help and position).`;\n if (f.whose === undefined) return undefined;\n if (!from.columns.some((column) => column.name === f.whose))\n return `Command input \"${f.name}\": \"${f.from}\" has no column \"${f.whose}\" for whose.`;\n if (!target || target.many) return `Command input \"${f.name}\" picks its questions by the record (whose), and a form's command has none.`;\n if (target.table.columns.filter((column) => column.primaryKey).length !== 1)\n return `Command input \"${f.name}\" picks its questions by the record's key, and ${target.table.id} has no single-column key.`;\n return undefined;\n}\n\n/* A record's lines: rows of a resource in this catalog, each column one a form writes, and each copy through a\n reference the line shows to a column of the record it names. */\nfunction linesInputProblem(f: Extract<AppCommandField, { type: 'lines' }>, schema: DatabaseSchema): string | undefined {\n const from = tableNamed(schema, f.from);\n if (!from) return `Command input \"${f.name}\" asks lines of \"${f.from}\", which is not a resource the command writes.`;\n for (const column of [...f.show, ...Object.keys(f.copy ?? {})]) {\n if (!from.columns.some((c) => c.name === column)) return `Command input \"${f.name}\": \"${column}\" is not a column of ${from.id}.`;\n /* Not the column's `writable`: a reader's catalog says that of a table this reader may not write, and the\n lines are written under the command's grant, not the reader's. Only what the App works out is never asked. */\n if (f.show.includes(column) && (from.computed?.[column] || from.rollups?.[column]))\n return `Command input \"${f.name}\": \"${column}\" is worked out, not written by a form.`;\n }\n for (const [column, path] of Object.entries(f.copy ?? {})) {\n const [via, read] = path.split('.');\n const target = from.columns.find((c) => c.name === via)?.references;\n const record = target && tableNamed(schema, target.table);\n if (!record) return `Command input \"${f.name}\" copies \"${column}\" through \"${via}\", which is not a reference of ${from.id}.`;\n if (!record.columns.some((c) => c.name === read))\n return `Command input \"${f.name}\" copies \"${column}\" from \"${read}\", which is not a column of ${record.id}.`;\n }\n for (const [column, condition] of Object.entries(f.query ?? {})) {\n const target = from.columns.find((c) => c.name === column)?.references;\n const record = target && tableNamed(schema, target.table);\n if (!record) return `Command input \"${f.name}\" narrows \"${column}\", which is not a reference of ${from.id}.`;\n /* Read as the picker reads it: a plain query, else a view (a path or a rollup), as a reference's query is. */\n const plain = parseQuery(`${record.id} ${condition}`, { schema, me: ANYONE });\n const view = plain.errors.length ? parseViewQuery(schema, record.id, condition, undefined, { me: ANYONE }) : undefined;\n if (view?.errors.length) return `Command input \"${f.name}\" query \"${column}\": ${view.errors[0].message}`;\n }\n return undefined;\n}\n\n/** The reference inputs a command asks, against the catalog: a form's command has only these to check. */\nexport function commandInputProblem(\n input: readonly AppCommandField[],\n schema: DatabaseSchema,\n target?: { table: TableMeta; many: boolean },\n): string | undefined {\n for (const { field: f } of commandFieldsDeep(input)) {\n const start = f.type === 'group' ? undefined : f.default;\n if (start && typeof start === 'object' && 'record' in start && target && !target.table.columns.some((c) => c.name === start.record))\n return `Command input \"${f.name}\" starts from record.${start.record}, which is not a column of ${target.table.id}.`;\n if (f.type === 'defined') {\n const problem = definedInputProblem(f, schema, target);\n if (problem) return problem;\n continue;\n }\n if (f.type === 'lines') {\n const problem = linesInputProblem(f, schema);\n if (problem) return problem;\n continue;\n }\n if ((f.type === 'choice' || f.type === 'choices') && f.source) {\n const from = tableNamed(schema, f.source.from);\n if (!from)\n return `Command input \"${f.name}\" offers the rows of \"${f.source.from}\", which is not a resource the command reads: add it to \\`read\\`.`;\n for (const column of [f.source.by, f.source.labelColumn])\n if (!from.columns.some((c) => c.name === column)) return `Command input \"${f.name}\": \"${f.source.from}\" has no column \"${column}\".`;\n continue;\n }\n if (f.type !== 'reference') continue;\n const resource = tableNamed(schema, f.from);\n if (!resource || !resource.columns.some((column) => column.name === f.labelColumn))\n return (\n `Command input \"${f.name}\" has an unresolved reference resource or label column. ` +\n (resource\n ? `\"${f.from}\" has no column \"${f.labelColumn}\": name one it has.`\n : `Most likely \"${f.from}\" is not in the command's \\`read\\` (a picker reads only the tables the command reads): add \"${f.from}\" to \\`read\\`, or check the name.`)\n );\n const keys = resource.columns.filter((column) => column.primaryKey);\n if (keys.length !== 1 || keys[0].name !== f.by) return `Command input \"${f.name}\" must select a single primary key.`;\n if (f.query) {\n const named = recordColumnsIn(f.query);\n if (named.length && (!target || target.many))\n return `Command input \"${f.name}\" query names record.${named[0]}, but ${target ? \"a command that acts on many records (targets: 'many')\" : \"a form's command\"} has no one record to read it from.`;\n const columns = named.map((name) => target!.table.columns.find((column) => column.name === name));\n const unknown = named.find((_, i) => !columns[i]);\n if (unknown) return `Command input \"${f.name}\" query names record.${unknown}, which is not a column of ${target!.table.id}.`;\n const asked = named.length\n ? boundRecordQuery(f.query, Object.fromEntries(columns.map((column) => [column!.name, SAMPLE[columnKind(column!.type)]])))!\n : f.query;\n /* The picker's read takes the query as a view where it is not a plain\n one (the /api/run fall-through), so a path or a rollup — \"released\n = 0\" on an order — is judged as that read will judge it. */\n // Judged before any request: the picker's read asks it as the reader, so `me` here is anyone's.\n const query = parseQuery(`${resource.id} ${asked}`, { schema, me: ANYONE });\n const view = query.errors.length ? parseViewQuery(schema, resource.id, asked, undefined, { me: ANYONE }) : undefined;\n if (view?.errors.length) return `Command input \"${f.name}\" query: ${view.errors[0].message}`;\n }\n }\n return undefined;\n}\n","/**\n * An expression's words read into its tree (computed.ts): the tokenizer and the parser computed columns, a form's\n * live lines and the App's checks read with. A module of its own because nothing in computed.ts calls it, and a\n * browser's first page reaches computed.ts (a computed column's meta): what a part loaded on demand uses of a module\n * the first page reaches is drawn into the first page's chunk, so the parser lived there for a form's live lines.\n */\nimport { ComputedError, type ComputedFunction, type ComputedNode, type ComputedOp, type ComputedUnit } from './computed.js';\n\n/** The functions an expression may call: each written alike on every engine and worked out alike in JavaScript. */\nconst COMPUTED_FUNCTIONS = ['coalesce', 'round', 'min', 'max', 'abs'] as const satisfies readonly ComputedFunction[];\nconst isFunction = (word: string): word is ComputedFunction => (COMPUTED_FUNCTIONS as readonly string[]).includes(word);\n/* The greatest number of places `round` keeps: past it a figure is no longer one an engine says exactly. */\nconst ROUND_PLACES = 10;\n\nconst CONSTANTS = new Set(['today', 'now', 'me', 'true', 'false']);\n\ntype Token = { t: 'word' | 'number' | 'text' | 'op' | 'end'; v: string; at: number };\n\nconst UNITS: Record<string, ComputedUnit> = {\n minute: 'minute',\n minutes: 'minute',\n hour: 'hour',\n hours: 'hour',\n day: 'day',\n days: 'day',\n week: 'week',\n weeks: 'week',\n month: 'month',\n months: 'month',\n year: 'year',\n years: 'year',\n};\nconst WORD = /[A-Za-z_][A-Za-z0-9_]*(?:\\.[A-Za-z_$][A-Za-z0-9_$]*)*/y;\n\nfunction tokens(text: string): Token[] {\n const out: Token[] = [];\n let i = 0;\n while (i < text.length) {\n const c = text[i];\n if (/\\s/.test(c)) {\n i++;\n continue;\n }\n if (/\\d/.test(c)) {\n const m = /\\d+(?:\\.\\d+)?/y;\n m.lastIndex = i;\n const n = m.exec(text)![0];\n out.push({ t: 'number', v: n, at: i });\n i += n.length;\n continue;\n }\n if (c === \"'\") {\n let j = i + 1,\n value = '';\n for (;;) {\n if (j >= text.length) throw new ComputedError('a quoted text is not closed.', i);\n if (text[j] === \"'\") {\n if (text[j + 1] === \"'\") {\n value += \"'\";\n j += 2;\n continue;\n }\n break;\n }\n value += text[j++];\n }\n /* Written into the statement between quotes, so held to what every engine reads alike there (recordLabel.ts). */\n if ([...value].some((ch) => ch === '\\\\' || ch.charCodeAt(0) < 0x20 || ch.charCodeAt(0) === 0x7f)) {\n throw new ComputedError('a quoted text holds a backslash or a control character, which engines read differently.', i);\n }\n out.push({ t: 'text', v: value, at: i });\n i = j + 1;\n continue;\n }\n const two = text.slice(i, i + 2);\n if (['!=', '<>', '<=', '>='].includes(two)) {\n out.push({ t: 'op', v: two === '<>' ? '!=' : two, at: i });\n i += 2;\n continue;\n }\n if ('=<>+-*/(),'.includes(c)) {\n out.push({ t: 'op', v: c, at: i });\n i++;\n continue;\n }\n WORD.lastIndex = i;\n const w = WORD.exec(text);\n if (w) {\n out.push({ t: 'word', v: w[0], at: i });\n i += w[0].length;\n continue;\n }\n throw new ComputedError(`\"${c}\" is not part of an expression.`, i);\n }\n out.push({ t: 'end', v: '', at: text.length });\n return out;\n}\n\n/**\n * An expression's tree, or why it is not one. `isColumn` says which bare\n * words are names: a word on the right of `=` or `!=` that is not one is a\n * value, as it is in a query (`status = open`).\n */\nexport function parseComputed(text: string, isColumn: (path: string) => boolean): ComputedNode {\n const list = tokens(text);\n let p = 0;\n const peek = () => list[p];\n const word = (v: string) => peek().t === 'word' && peek().v.toLowerCase() === v;\n const take = () => list[p++];\n const expect = (v: string) => {\n if (peek().t !== 'op' || peek().v !== v)\n throw new ComputedError(`expected \"${v}\"${peek().t === 'end' ? ' before the end' : ` at \"${peek().v}\"`}.`, peek().at);\n take();\n };\n const or = (): ComputedNode => {\n let left = and();\n while (word('or')) {\n take();\n left = { k: 'op', op: 'or', left, right: and() };\n }\n return left;\n };\n const and = (): ComputedNode => {\n let left = not();\n while (word('and')) {\n take();\n left = { k: 'op', op: 'and', left, right: not() };\n }\n return left;\n };\n const not = (): ComputedNode => {\n if (word('not')) {\n take();\n return { k: 'not', of: not() };\n }\n return compare();\n };\n const compare = (): ComputedNode => {\n const left = additive();\n if (word('is')) {\n take();\n const negate = word('not');\n if (negate) take();\n if (!word('empty')) throw new ComputedError('say \"is empty\" or \"is not empty\".', peek().at);\n take();\n return { k: 'empty', of: left, negate };\n }\n /* `status in (open, waiting)`, `status not in (done)`: the query language's list, said as the equalities it\n means, joined by \"or\", so it types, compiles on every engine and works out in the browser as `=` does. */\n const negated = word('not') && list[p + 1].t === 'word' && list[p + 1].v.toLowerCase() === 'in';\n if (negated || word('in')) {\n if (negated) take();\n take();\n expect('(');\n const items: ComputedNode[] = [];\n for (;;) {\n const valueAt = peek();\n const next = list[p + 1];\n items.push(\n left.k === 'path' &&\n valueAt.t === 'word' &&\n !CONSTANTS.has(valueAt.v.toLowerCase()) &&\n !valueAt.v.includes('.') &&\n !isColumn(valueAt.v) &&\n next.t === 'op' &&\n (next.v === ',' || next.v === ')')\n ? (take(), { k: 'text', value: valueAt.v })\n : additive(),\n );\n if (peek().t === 'op' && peek().v === ',') {\n take();\n continue;\n }\n expect(')');\n break;\n }\n const any = items\n .map((right): ComputedNode => ({ k: 'op', op: '=', left, right }))\n .reduce((either, each): ComputedNode => ({ k: 'op', op: 'or', left: either, right: each }));\n return negated ? { k: 'not', of: any } : any;\n }\n const op = peek();\n if (op.t === 'op' && ['=', '!=', '<', '<=', '>', '>='].includes(op.v)) {\n take();\n const valueAt = peek();\n /* `status = open`: a bare word that names no column is the value, as in a query. */\n const right =\n (op.v === '=' || op.v === '!=') &&\n left.k === 'path' &&\n valueAt.t === 'word' &&\n !CONSTANTS.has(valueAt.v.toLowerCase()) &&\n !valueAt.v.includes('.') &&\n !isColumn(valueAt.v) &&\n (list[p + 1].t === 'end' || list[p + 1].t === 'word' || (list[p + 1].t === 'op' && list[p + 1].v === ')'))\n ? (take(), { k: 'text', value: valueAt.v } as ComputedNode)\n : additive();\n if (op.v === '=' && right.k === 'path' && /^null$/i.test(right.path))\n throw new ComputedError('\"= null\" is never true; say \"is empty\".', valueAt.at);\n return { k: 'op', op: op.v as ComputedOp, left, right };\n }\n return left;\n };\n const additive = (): ComputedNode => {\n let left = multiplicative();\n while (peek().t === 'op' && (peek().v === '+' || peek().v === '-')) {\n const op = take().v as ComputedOp;\n left = { k: 'op', op, left, right: multiplicative() };\n }\n return left;\n };\n const multiplicative = (): ComputedNode => {\n let left = unary();\n while (peek().t === 'op' && (peek().v === '*' || peek().v === '/')) {\n const op = take().v as ComputedOp;\n left = { k: 'op', op, left, right: unary() };\n }\n return left;\n };\n const unary = (): ComputedNode => {\n if (peek().t === 'op' && peek().v === '-') {\n take();\n return { k: 'neg', of: unary() };\n }\n const value = primary();\n /* `3 days`, `term months`: a span a date moves by. */\n if (peek().t === 'word' && Object.hasOwn(UNITS, peek().v.toLowerCase()))\n return { k: 'span', of: value, unit: UNITS[take().v.toLowerCase()] };\n return value;\n };\n const primary = (): ComputedNode => {\n const token = take();\n if (token.t === 'number') return { k: 'number', value: Number(token.v) };\n if (token.t === 'text') return { k: 'text', value: token.v };\n if (token.t === 'op' && token.v === '(') {\n const inner = or();\n expect(')');\n return inner;\n }\n if (token.t === 'word') {\n const lower = token.v.toLowerCase();\n /* `round(total, 2)`: a function's name is a word followed by \"(\", which no column or value ever is. */\n if (peek().t === 'op' && peek().v === '(') {\n if (!isFunction(lower))\n throw new ComputedError(`\"${token.v}\" is not a function; an expression calls ${COMPUTED_FUNCTIONS.join(', ')}.`, token.at);\n take();\n const args: ComputedNode[] = [];\n if (!(peek().t === 'op' && peek().v === ')'))\n for (;;) {\n args.push(or());\n if (peek().t === 'op' && peek().v === ',') {\n take();\n continue;\n }\n break;\n }\n expect(')');\n const fn = lower;\n const many = fn === 'coalesce' || fn === 'min' || fn === 'max';\n if (many ? args.length < 2 : fn === 'round' ? args.length < 1 || args.length > 2 : args.length !== 1)\n throw new ComputedError(\n many\n ? `${fn}(…) takes two values or more: ${fn}(a, b).`\n : fn === 'round'\n ? 'round(…) takes a number and, if you like, how many places: round(total) or round(total, 2).'\n : `${fn}(…) takes one number.`,\n token.at,\n );\n if (fn === 'round' && args[1] && !(args[1].k === 'number' && Number.isInteger(args[1].value) && args[1].value <= ROUND_PLACES))\n throw new ComputedError(\n `round(…)'s places are a whole number from 0 to ${ROUND_PLACES}, written out: round(total, 2).`,\n token.at,\n );\n return { k: 'call', fn, args };\n }\n if (lower === 'true' || lower === 'false') return { k: 'bool', value: lower === 'true' };\n if (lower === 'today' || lower === 'now' || lower === 'me') return { k: lower };\n if (['and', 'or', 'not', 'is', 'empty', 'in'].includes(lower) || Object.hasOwn(UNITS, lower))\n throw new ComputedError(`\"${token.v}\" stands where a value goes.`, token.at);\n return { k: 'path', path: token.v };\n }\n throw new ComputedError(\n token.t === 'end' ? 'the expression ends where a value goes.' : `\"${token.v}\" stands where a value goes.`,\n token.at,\n );\n };\n const tree = or();\n if (peek().t !== 'end') throw new ComputedError(`\"${peek().v}\" follows a complete expression; join two with \"and\" or \"or\".`, peek().at);\n return tree;\n}\n","/**\n * A funnel over rows (`funnel()`, a dashboard section): how many of the rows a section holds reach each of its stages,\n * in order. The stages are conditions on the one table; a row reaches stage k when it holds the section's own query and\n * every required stage up to k. An optional stage is counted against the chain before it, and the stages after it do\n * not need it — so one key covers a strict road and an any-order checklist (every stage optional).\n *\n * Every stage is counted in the one statement (`count when [...]`, breakdown.ts): one snapshot, so a later stage can\n * never exceed an earlier one under concurrent writes, which separate counts could.\n *\n * Pure text in, text out: the client draws from it, the server asks from it for a first page (firstPaintRooms.ts), and\n * `mistri check` judges with it.\n */\nimport { andQuery, topLevelConditions } from './query.js';\n\nexport interface FunnelStage {\n /** The stage as a reader says it, said from the catalogs by the time it is parsed. */\n label: string;\n /** Conditions only (segments are words in it); absent, every row the section holds. */\n query?: string;\n /** Counted against the chain before it, and not required by the stages after it. */\n optional?: boolean;\n}\n\nexport const FUNNEL_MIN_STAGES = 2;\nexport const FUNNEL_MAX_STAGES = 8;\n\n/** The answer's column for stage `index` (0-based): the alias its measure is asked under. */\nexport const stageColumn = (index: number): string => `stage_${index + 1}`;\n\n/**\n * The conditions of each stage's reached rows: the required stages before it, then its own. Empty text for a stage\n * that asks nothing of the rows, which are all of them.\n */\nexport function funnelChains(stages: readonly Pick<FunnelStage, 'query' | 'optional'>[]): string[] {\n const required: string[] = [];\n return stages.map((stage) => {\n const chain = andQuery(...required, stage.query);\n if (!stage.optional && stage.query?.trim()) required.push(stage.query);\n return chain;\n });\n}\n\n/**\n * The stage each stage is a share of: the last required stage before it. The first stage has none. A funnel whose\n * first stage is optional is refused where it is parsed (appViews.ts), so every later stage has one.\n */\nexport function funnelBasis(stages: readonly Pick<FunnelStage, 'optional'>[]): Array<number | null> {\n let last: number | null = null;\n return stages.map((stage, index) => {\n const basis = last;\n if (!stage.optional) last = index;\n return basis;\n });\n}\n\n/** The line a funnel asks: its table, the section's conditions, then one count per stage. */\nexport function funnelText(section: { table?: string; query?: string; stages?: readonly FunnelStage[] }): string | null {\n if (!section.table || !section.stages?.length) return null;\n const measures = funnelChains(section.stages).map((chain, index) =>\n chain ? `count when [${chain}] as ${stageColumn(index)}` : `count as ${stageColumn(index)}`,\n );\n return [section.table, section.query, measures.join(', ')].filter(Boolean).join(' ');\n}\n\nconst COMPARE_OPPOSITE: Record<string, string> = { '=': '!=', '!=': '=', '>': '<=', '>=': '<', '<': '>=', '<=': '>' };\n/* A value that is a phrase of the date grammar or a clock word, not a plain value: its opposite is not what `!=` says. */\nconst NOT_PLAIN_VALUE = /^(today|yesterday|tomorrow|now|this|next|last|previous)$/i;\nconst VALUE = String.raw`(\"[^\"]*\"|'[^']*'|[^\\s\"',()]+)`;\n\n/**\n * One condition's opposite, or undefined where the language cannot say it exactly. The opposite is exact for a row\n * with nothing in the column too: `status = approved` is not `status != approved` (a SQL comparison skips an empty\n * value), it is `status != approved or status is empty`. Unsaid on purpose: a phrase of dates, a nested `or`, `has`,\n * anything the language has no word for, since the drill must open the rows the figure counted and no others.\n */\nexport function negateCondition(condition: string): string | undefined {\n const text = condition.replace(/\\s+/g, ' ').trim();\n if (!text || text.startsWith('(')) return undefined;\n const rows = /^(any|no) (.+)$/i.exec(text);\n if (rows) return `${rows[1].toLowerCase() === 'any' ? 'no' : 'any'} ${rows[2]}`;\n const empty = /^([\\w.$+]+) is (not )?empty$/i.exec(text);\n if (empty) return `${empty[1]} is ${empty[2] ? '' : 'not '}empty`;\n const compare = new RegExp(String.raw`^([\\w.$+]+) (=|!=|>=|<=|>|<) ${VALUE}$`).exec(text);\n if (compare && !NOT_PLAIN_VALUE.test(compare[3]))\n return `${compare[1]} ${COMPARE_OPPOSITE[compare[2]]} ${compare[3]} or ${compare[1]} is empty`;\n const among = /^([\\w.$+]+) in \\((.+)\\)$/i.exec(text);\n if (among) {\n const values = among[2].match(new RegExp(String.raw`${VALUE}(?=\\s*(,|$))`, 'g'));\n const whole = new RegExp(String.raw`^\\s*${VALUE}(\\s*,\\s*${VALUE})*\\s*$`);\n if (!values?.length || !whole.test(among[2]) || values.some((value) => NOT_PLAIN_VALUE.test(value))) return undefined;\n return `${values.map((value) => `${among[1]} != ${value}`).join(' and ')} or ${among[1]} is empty`;\n }\n return undefined;\n}\n\n/**\n * The rows that do not hold a query, or undefined when any of its conditions cannot be turned over exactly. A query\n * is its conditions all at once, so its opposite is any one of their opposites.\n */\nexport function negateQuery(query: string, segments?: Readonly<Record<string, string>>): string | undefined {\n const held = topLevelConditions(query, segments);\n if (!held.length) return undefined;\n const opposites = held.map(negateCondition);\n return opposites.every((text): text is string => text !== undefined) ? opposites.join(' or ') : undefined;\n}\n\n/**\n * The rows a stage lost: those that reached the stage it is a share of and do not hold its own conditions. Only a\n * required stage drops rows (an optional one is a side road), and only where its conditions can be turned over.\n */\nexport function droppedQuery(\n stages: readonly FunnelStage[],\n index: number,\n segments?: Readonly<Record<string, string>>,\n): string | undefined {\n const stage = stages[index];\n if (!stage || stage.optional || index === 0 || !stage.query?.trim()) return undefined;\n const opposite = negateQuery(stage.query, segments);\n if (!opposite) return undefined;\n const before = stages.slice(0, index).flatMap((earlier) => (!earlier.optional && earlier.query?.trim() ? [earlier.query] : []));\n return andQuery(...before, opposite);\n}\n","/**\n * A page's archetype: `page(…, { layout })`, the arrangement of its regions.\n *\n * Every record page used to be one skeleton, a main column with an aside a\n * third of its width beside it, and a look could recolour that skeleton but\n * never move it. Three kinds of record read badly in it: one worked by\n * talking (the thread was pushed down by the lists above it), one chosen from\n * photos (on a phone the asking rent and its call to action fell to the\n * bottom, after every list) and one that is mostly words (a full-width column\n * with no measure). Each is a word here. A layout decides which regions a\n * page has, what each may hold and the DOM order; it never sets a colour, a\n * rule or a type size, which stay the looks' recipes.\n *\n * The words are refused where they cannot hold: what a layout cannot place is\n * said by the section and with the fix, so a page never draws an arrangement\n * nobody asked for.\n *\n * A page that names none is given the one its sections already make\n * (chooseLayout): a gallery first is a showcase, a timeline the main column\n * ends with a conversation, a page of the record's own words an article, and\n * anything else the record's skeleton, `'record'`. Chosen when the App is\n * read, from what the page declares, never from a row: a page keeps one shape\n * for every record and from its first frame. Writing a word is still worth\n * it: a written layout refuses a section it cannot place, where a chosen one\n * falls back to the record; `'record'` is how a page that would be chosen as\n * something else keeps the skeleton.\n */\nimport type { PageDef, PageSection } from './pageModel.js';\nimport { isLongText, MAX_PROPERTIES } from './pageAnatomy.js';\n\n/** The archetypes a page is drawn in. The record's own skeleton draws no `data-layout`. */\nexport type PageLayoutName = 'conversation' | 'showcase' | 'article';\nconst PAGE_LAYOUTS: readonly PageLayoutName[] = ['conversation', 'showcase', 'article'];\n/** What a page may say: an archetype, or `'record'`, the skeleton, kept where another would be chosen. */\nexport type PageLayoutWord = 'record' | PageLayoutName;\nexport const PAGE_LAYOUT_WORDS: readonly PageLayoutWord[] = ['record', ...PAGE_LAYOUTS];\n\n/* A showcase's panel stands beside the photos and ends with them: more than three sections there grew the band\n past the gallery's height at a desk, and the body under it stood a screen down. */\nconst PANEL_MOST = 3;\n\nconst WORDS =\n \"'record' (the record's own: its lists beside a rail), 'conversation' (a thread with its facts beside it), 'showcase' (photos with the price beside them) or 'article' (one column at a reading measure)\";\n\n/** `layout`, read: one of the words, else refused with all four. */\nexport function parsePageLayout(raw: unknown, at: string): PageLayoutWord {\n if (PAGE_LAYOUT_WORDS.includes(raw as PageLayoutWord)) return raw as PageLayoutWord;\n /* Renamed before 1.0: \"listing\" is what a list view is called everywhere else in an App. */\n if (raw === 'listing')\n throw new Error(`[page-layout] ${at} has \"layout\": \"listing\", which is now \"showcase\": photos with the price beside them.`);\n throw new Error(\n `[page-layout] ${at} has \"layout\": ${JSON.stringify(raw)}. A page's layout is ${WORDS}; leave it out for the one its sections make.`,\n );\n}\n\n/** The layout a renderer draws: an archetype, or none for the record's own. */\nexport function drawnLayout(page: { layout?: string }): PageLayoutName | undefined {\n return PAGE_LAYOUTS.find((word) => word === page.layout);\n}\n\n/* Structural, so the client's own page shapes (components/pageModel.ts) are read by the same rule. */\ntype Placed = {\n kind: string;\n region?: string;\n presentation?: unknown;\n hidden?: boolean;\n};\nconst isHeader = (section: Placed) => section.kind === 'fields' && section.presentation === 'header';\nconst inMain = (section: Placed) => (section.region ?? 'main') === 'main' && !isHeader(section) && !section.hidden;\nconst inAside = (section: Placed) => section.region === 'aside' && !section.hidden;\n\n/**\n * A showcase's lead: the index of its first main section, the photos, which stand beside the panel. The head\n * is drawn above it, so a header() is not the first.\n */\nexport function showcaseIndex(page: { layout?: string; sections: readonly Placed[] }): number | undefined {\n if (page.layout !== 'showcase') return undefined;\n const index = page.sections.findIndex(inMain);\n return index < 0 ? undefined : index;\n}\n\n/** A section as a refusal names it: its kind and, where it has one, its title. */\nfunction named(section: PageSection): string {\n const title = 'title' in section && typeof section.title === 'string' ? ` \"${section.title}\"` : '';\n if (section.kind === 'fields') {\n const kind = section.presentation === 'card' || section.presentation === 'headline' ? section.presentation : 'fields';\n return `a ${kind}${title}`;\n }\n if (section.kind === 'list') return `${section.view !== undefined ? 'an embed' : 'a related list'}${title}`;\n return `a ${section.kind}${title}`;\n}\n\n/**\n * Judge a parsed App page against its layout, and place what the layout places unasked. `at` names the page as\n * the App's other page refusals do. Mutates `page`: under `conversation` a headline that names no region stands\n * in the aside, as a card and the history already do; under `showcase` and `article` no default rail is drawn.\n */\nexport function applyPageLayout(page: PageDef, at: string): void {\n const layout = drawnLayout(page);\n if (!layout) return;\n const each = page.sections.map((section, index) => ({\n section,\n index,\n where: `${at}, section ${index + 1}`,\n }));\n\n if (layout === 'conversation') {\n const say = (text: string) => new Error(`[layout-conversation] ${text}`);\n const fix = \"A conversation is its one timeline, last in main: put the rest in region: 'aside'\";\n for (const { section } of each) {\n if (section.kind === 'fields' && section.presentation === 'headline' && section.region === undefined) section.region = 'aside';\n }\n const timelines = each.filter(({ section }) => section.kind === 'timeline' && !section.hidden);\n if (!timelines.length)\n throw say(\n `${at} is a conversation with no timeline. ${fix}, and add timeline('Conversation', [stream(…)]) to the main column, or leave out \"layout\".`,\n );\n if (timelines.length > 1)\n throw say(\n `${at} is a conversation with ${timelines.length} timelines. A conversation is its one timeline: say every stream in one timeline('…', [stream(…), stream(…)]).`,\n );\n const thread = timelines[0];\n if (thread.section.region === 'aside')\n throw say(`${thread.where} puts the conversation's timeline in the aside. ${fix}; leave its region out.`);\n for (const { section, where } of each) {\n if (!inMain(section)) continue;\n if (section.tab !== undefined) {\n throw say(\n `${where} names the tab \"${section.tab}\" in the main column. A thread is not one tab among others: leave out \"tab\", or give it to a section in region: 'aside'.`,\n );\n }\n if (section.kind === 'list' || section.kind === 'metric' || section.kind === 'chart' || section.kind === 'breakdown') {\n throw say(\n `${where} is ${named(section)} in the main column. ${fix}: ${section.kind === 'list' ? `related(${typeof section.title === 'string' ? `'${section.title}'` : '…'}, …, { region: 'aside' })` : \"{ region: 'aside' }\"}.`,\n );\n }\n }\n const after = each.find(({ section, index }) => index > thread.index && inMain(section));\n if (after) {\n throw say(\n `${after.where} (${named(after.section)}) stands after the timeline in the main column. ${fix}, or above the timeline, so the composer ends the thread.`,\n );\n }\n return;\n }\n\n if (layout === 'showcase') {\n const say = (text: string) => new Error(`[layout-showcase] ${text}`);\n const fix =\n \"A showcase opens with its photos: related('Photos', …, { presentation: 'gallery' }) first, its price headline in the aside\";\n if (page.aside === false)\n throw say(`${at} is a showcase that says aside: false. A showcase is its panel beside the photos: leave out \"layout\" instead.`);\n const first = each.find(({ section }) => inMain(section));\n if (!first || first.section.kind !== 'list' || first.section.presentation !== 'gallery') {\n throw say(\n `${first ? `${first.where} is ${named(first.section)}, first in the main column` : `${at} has nothing in its main column`}. ${fix}.`,\n );\n }\n const panel = each.filter(({ section }) => inAside(section));\n if (!panel.length) throw say(`${at} is a showcase with nothing in the aside. ${fix}: headline('…', { region: 'aside' }).`);\n if (panel.length > PANEL_MOST) {\n throw say(\n `${at} has ${panel.length} sections in the aside, and a showcase's panel holds 1 to ${PANEL_MOST} beside the photos: move the rest into the main column, where they stand under the photos at full width.`,\n );\n }\n const tabbed = panel.find(({ section }) => section.tab !== undefined);\n if (tabbed)\n throw say(\n `${tabbed.where} names the tab \"${tabbed.section.tab}\" in the aside. A showcase's panel stands beside the photos whole: leave out \"tab\".`,\n );\n // The panel is a price, not a property rail: the author's own fields say what a buyer reads.\n delete page.defaultAside;\n return;\n }\n\n // article\n const say = (text: string) => new Error(`[layout-article] ${text}`);\n if (page.aside === false) throw say(`${at} is an article that says aside: false. An article has no aside already: remove \"aside\".`);\n for (const { section, where } of each) {\n if (section.region === 'aside') {\n const call =\n section.kind === 'fields' && section.presentation === 'card' && typeof section.title === 'string'\n ? `card('${section.title}', …)`\n : named(section);\n throw say(\n `${where} (${named(section)}) stands in the aside. An article has no aside: move ${call} into the column with region: 'main', or leave out \"layout\".`,\n );\n }\n if (section.width === 'third' || section.width === 'two-thirds') {\n throw say(\n `${where} has width: '${section.width}'. An article is one column at a reading measure, where a third is too narrow for a value: use 'half' or leave \"width\" out.`,\n );\n }\n }\n delete page.defaultAside;\n}\n\n/** A refusal as a reason: its text without the code, which names a word the author did not write. */\nfunction refusal(page: PageDef, layout: PageLayoutName): string | undefined {\n // The rule's own mutations (a headline beside the thread, no default rail) land on the copy.\n const trial: PageDef = {\n ...page,\n layout,\n sections: page.sections.map((section) => ({ ...section }) as PageSection),\n };\n try {\n applyPageLayout(trial, 'the page');\n return undefined;\n } catch (error) {\n return (error as Error).message.replace(/^\\[[a-z-]+\\]\\s*/, '');\n }\n}\n\nconst quoted = (section: PageSection) => ('title' in section && typeof section.title === 'string' ? ` \"${section.title}\"` : '');\n\n/**\n * The layout a page's sections already make, where it names none. Each archetype is chosen by the section it is\n * named for — a showcase by the photos it opens with, a conversation by the timeline it ends with, an article by\n * the record's long text with nothing but its facts and notes around it — and only where applyPageLayout takes the\n * page as written, so a chosen layout is never a refusal. The first that holds wins: photos lead the page they open,\n * a thread ends one. Read from the declarations alone, never a row, so every record of a page has one shape from the\n * first frame. Anything else is the record's own skeleton. The why is what `mistri check --defaults` says.\n */\nexport function chooseLayout(page: PageDef): { layout: PageLayoutWord; why: string } {\n const main = page.sections.filter(inMain);\n const missed: string[] = [];\n const first = main[0];\n if (first?.kind === 'list' && first.presentation === 'gallery') {\n const no = refusal(page, 'showcase');\n if (!no)\n return {\n layout: 'showcase',\n why: `it opens with the gallery${quoted(first)}, with a panel beside it`,\n };\n missed.push(`it opens with a gallery, but a showcase would refuse it: ${no}`);\n } else missed.push('it opens with no gallery');\n const thread = main.find((section) => section.kind === 'timeline');\n if (thread) {\n const no = refusal(page, 'conversation');\n if (!no)\n return {\n layout: 'conversation',\n why: `its main column ends with the timeline${quoted(thread)}, and no list stands in it`,\n };\n missed.push(`it has a timeline, but a conversation would refuse it: ${no}`);\n } else missed.push('it has no timeline');\n const shown = page.sections.filter((section) => !section.hidden);\n const words = shown.every(\n (section) =>\n section.kind === 'note' || (section.kind === 'fields' && section.presentation !== 'card' && section.presentation !== 'headline'),\n );\n // The record's own long text, read in the column; a header's facts are a line under its name, never the words.\n const long = shown.flatMap((section) =>\n section.kind === 'fields' && section.presentation !== 'header'\n ? (section.paths ?? []).filter((path) => !path.includes('.') && isLongText(path))\n : [],\n );\n const facts = [...new Set(shown.flatMap((section) => (section.kind === 'fields' ? (section.paths ?? []) : [])))].filter(\n (path) => !long.includes(path),\n ).length;\n if (!words) missed.push('it holds more than fields and notes');\n else if (!long.length) missed.push('it names no long text, a description or a body');\n // As many facts as a rail holds: more is a record with a note on it, which keeps its rail.\n else if (facts > MAX_PROPERTIES) missed.push(`it has ${facts} facts beside its words, more than a rail's ${MAX_PROPERTIES}`);\n else {\n const no = refusal(page, 'article');\n if (!no)\n return {\n layout: 'article',\n why: `it is the record's words, ${long.map((path) => `\"${path}\"`).join(' and ')}, with ${facts} fact${facts === 1 ? '' : 's'} and nothing else`,\n };\n missed.push(`it is the record's words, but an article would refuse it: ${no}`);\n }\n return { layout: 'record', why: missed.join('; ') };\n}\n","/**\n * A record page's plan and every read its sections will ask, in one request\n * (shared/recordReads.ts). The reads are kept for the sections to find\n * (composedReads.ts); the plan is what the room draws from.\n *\n * A press on a row or a link to a record starts it (recordPress.ts), so\n * the request is on its way while the button is still down, and the room\n * that opens takes the one already running instead of asking again.\n */\nimport { api } from './core.js';\nimport { ownedBy } from './requestIdentity.js';\nimport { relatedRows } from './relatedRows.js';\nimport { composedEpoch, composing, holdComposed, type ComposedRead } from './composedReads.js';\nimport { sessionIdentity } from './sessionIdentity.js';\nimport { RECORD_COMPOSE_PATH } from '@mistriwork/engine/shared/recordReads.js';\nimport { canonicalText } from '@mistriwork/engine/shared/recordReadKeys.js';\nimport type { PageDef } from '@mistriwork/engine/shared/page.js';\nimport type { SectionPlan } from './components/pageModel.js';\n\ninterface Planned {\n page?: PageDef;\n plans?: SectionPlan[];\n}\n\n/* A compose begun by a press, until the room it opens takes it; a press\n that opened nothing lets it go. */\nconst begun = new Map<string, { work: Promise<Planned>; until: number; taken?: boolean }>();\nconst KEPT = 10_000;\n\nconst identityOf = (connection: string, page: string | PageDef, key: Record<string, unknown>, tab: string | undefined) =>\n canonicalText([connection, sessionIdentity().epoch, page, key, tab ?? null, relatedRows()]);\n\nfunction send(connection: string, page: string | PageDef, key: Record<string, unknown>, tab: string | undefined): Promise<Planned> {\n // Taken before the request leaves: a write sent or answered while it is out makes its answers the past.\n const since = composedEpoch();\n const work = (async (): Promise<Planned> => {\n let reply: { planned?: boolean; page?: PageDef; plans?: SectionPlan[]; reads?: ComposedRead[] } | undefined;\n try {\n reply = (await api(\n RECORD_COMPOSE_PATH as '/api/pages/plan',\n { ...ownedBy(connection), page, key, rows: relatedRows(), ...(tab === undefined ? {} : { tab }) } as never,\n )) as typeof reply;\n } catch {\n reply = undefined;\n }\n /* No plan came with it: the plan is asked alone, and says in its own words why it cannot be made. */\n if (!reply?.planned)\n return (await api('/api/pages/plan', { ...ownedBy(connection), page: page as never, key: key as never })) as Planned;\n holdComposed(connection, reply.reads ?? [], since);\n return { page: reply.page, plans: reply.plans };\n })();\n composing(connection, work);\n return work;\n}\n\n/** The page's plan, with its reads kept for the sections it draws on `tab`: the one a press began, or a new one. */\nexport function composeRecordPage(\n connection: string,\n page: string | PageDef,\n key: Record<string, unknown>,\n tab?: string,\n): Promise<Planned> {\n const id = identityOf(connection, page, key, tab);\n const early = begun.get(id);\n /* Taken once: the room's next read of its plan (a tab coming back) is a\n fresh one. Kept, taken, until it expires, so a press that lingers (its\n focus, its Enter) does not begin the page it just opened again. */\n if (early && !early.taken && early.until > Date.now()) {\n early.taken = true;\n return early.work;\n }\n return send(connection, page, key, tab);\n}\n\n/** Begin a record page's compose ahead of the navigation that will ask for it. */\nexport function beginRecordPage(connection: string, page: string, key: Record<string, unknown>): void {\n const id = identityOf(connection, page, key, undefined);\n const now = Date.now();\n for (const [kept, entry] of begun) if (entry.until <= now) begun.delete(kept);\n if (begun.has(id)) return;\n const work = send(connection, page, key, undefined);\n work.catch(() => {\n begun.delete(id);\n });\n begun.set(id, { work, until: now + KEPT });\n}\n","/** What recordSurface says, in the record page room (packages/kit/src/client/messages.ts says how the catalog is split). */\nimport { defineMessages } from '../../../messages.js';\n\nexport const say = defineMessages({\n thisRecordSurfaceIsNoLonger: 'This record surface is no longer current.',\n couldNotRefreshThisRecord: 'Could not refresh this record. Showing the last loaded layout.',\n incompleteSurfaceRoot: 'Incomplete surface root',\n thisSurfaceNeedsAComplete: 'This surface needs a complete record key and the page’s base table.',\n});\n","/** Shared page reads. A host owns presentation; this module never moves the walk,\n * changes a draft, settles a mutation or records a visit. */\nimport { ownedBy } from '../requestIdentity.js';\nimport { useMemo, useSyncExternalStore } from 'react';\nimport { useQuery } from '@tanstack/react-query';\nimport { api, findTable, labelPath, primaryKey, state, writeAllowed } from '../core.js';\nimport { shell } from '../shellMode.js';\nimport { grantedActionIds } from '../actionGrants.js';\nimport { connectionGeneration } from '../connectionTransition.js';\nimport { sessionIdentity, subscribeSessionIdentity } from '../sessionIdentity.js';\nimport { pageById, pagesFor } from '../page.js';\nimport { pathEqualityFilter, recordKey } from '../resourceRequests.js';\nimport type { TableMeta } from '@mistriwork/engine/shared/schema.js';\nimport type { PageDef } from '@mistriwork/engine/shared/page.js';\nimport type { PageProps } from './Page.js';\nimport { resourceOf } from './appRooms.js';\nimport { rootMatches, type PageEntry, type SectionPlan } from './pageModel.js';\nimport { useWalk } from './walk.js';\nimport { composeRecordPage } from '../recordCompose.js';\nimport { FOCUS_ALWAYS } from './mount.js';\nimport { say } from '../copy/record/components/recordSurface.js';\n\nexport type RecordSurfacePage = PageDef & { id: string; name: string; base: string; source: 'saved' | 'config' | 'suggested' };\nexport interface RecordSurfaceAlternative {\n id: string;\n name: string;\n source: string;\n}\nexport type RecordSurfaceEntry = PageEntry & { table: string; key: Record<string, unknown> };\n\n/** Capture once per host lifetime, not on every render. The host predicate must\n * fence close/reopen and replacement generations, including the same record.\n * It supplements the captured data authority; it cannot replace those checks. */\nconst capturedOwner = Symbol('record surface owner');\nexport interface RecordSurfaceOwner {\n readonly [capturedOwner]: true;\n readonly connection: string;\n readonly queryScope: number;\n readonly isCurrent: () => boolean;\n}\n\nlet nextScope = 0;\nexport function captureRecordSurfaceOwner(hostIsCurrent: () => boolean): RecordSurfaceOwner {\n const identity = sessionIdentity(),\n schema = state.schema;\n const connection = state.activeConnection ?? '',\n generation = connectionGeneration();\n const app = shell().app(),\n appMode = shell().inApp();\n return Object.freeze({\n [capturedOwner]: true as const,\n connection,\n queryScope: ++nextScope,\n isCurrent: () => {\n try {\n return (\n identity === sessionIdentity() &&\n identity.mode !== 'locked' &&\n identity.mode !== 'unknown' &&\n schema === state.schema &&\n connection === (state.activeConnection ?? '') &&\n generation === connectionGeneration() &&\n app === shell().app() &&\n appMode === shell().inApp() &&\n hostIsCurrent() === true\n );\n } catch {\n return false;\n }\n },\n });\n}\n\nclass RetiredSurface extends Error {\n constructor() {\n super(say('thisRecordSurfaceIsNoLonger'));\n }\n}\n\n/** Data for the existing PageView, including authored fixed actions and business\n * commands. Rendering a modal still requires host-aware PageView mutation and\n * dock integration: its current internal go() callbacks are route-specific. */\ntype RecordSurfaceContent = Pick<PageProps, 'entry' | 'page' | 'base' | 'plans' | 'others' | 'actions'>;\n\ntype RecordSurfaceState =\n | { status: 'retired' | 'loading' }\n | { status: 'error'; message: string }\n | { status: 'missing'; page: RecordSurfacePage; base: TableMeta }\n | { status: 'ready'; content: RecordSurfaceContent; refreshFailed: boolean };\n\nexport const RECORD_REFRESH_FAILED = say('couldNotRefreshThisRecord');\n\n/** Explicit non-route entry point. Never parses a URL, resolves a cold root or\n * falls back to the current route's entry. The host supplies label/tab changes\n * and consumes content only while this surface remains current. */\nexport function useRecordSurface({ owner, entry }: { owner: RecordSurfaceOwner; entry: RecordSurfaceEntry }) {\n const surface = usePageSurface(owner, entry, { explicitRoot: true });\n let result: RecordSurfaceState;\n if (!surface.isCurrent()) result = { status: 'retired' };\n else if (surface.definition.isError && !surface.page) result = { status: 'error', message: surface.definition.error.message };\n else if (!surface.page) result = { status: 'loading' };\n else if (surface.page.error) result = { status: 'error', message: surface.page.error };\n else if (!surface.base) result = { status: 'error', message: say('common.isNotATable', { base: surface.page.base }) };\n else if (surface.rootError) result = { status: 'error', message: surface.rootError };\n else if (surface.gone) result = { status: 'missing', page: surface.page, base: surface.base };\n else if (surface.plan.isError && !surface.content) result = { status: 'error', message: surface.plan.error.message };\n // Same-owner background errors cannot retire an editor or its unresolved\n // nested mutations. Cached data never crosses the ownership checks above.\n else if (surface.content) result = { status: 'ready', content: surface.content, refreshFailed: surface.refreshFailed };\n else result = { status: 'loading' };\n return { result, rootRow: surface.rootRow, isCurrent: surface.isCurrent };\n}\n\n/** Shared pipeline also used by PageRoom before its routed wrapper has resolved\n * a cold URL or chosen a root. Only that wrapper may mutate the routed entry. */\nexport function usePageSurface(owner: RecordSurfaceOwner, entry: PageEntry, options: { explicitRoot?: boolean } = {}) {\n useSyncExternalStore(subscribeSessionIdentity, sessionIdentity, sessionIdentity);\n useWalk();\n const keyText = JSON.stringify(entry.key);\n const target = useMemo(() => {\n const pageId = entry.pageId,\n table = entry.table,\n rootBody = entry.rootBody;\n const key = entry.key ? Object.freeze({ ...entry.key }) : undefined;\n const isCurrent = () =>\n owner[capturedOwner] === true &&\n owner.isCurrent() &&\n entry.pageId === pageId &&\n entry.table === table &&\n entry.rootBody === rootBody &&\n JSON.stringify(entry.key) === keyText;\n // Check after rejection as well as fulfillment: stale failures cannot become\n // a missing-record state or start another request under a replacement owner.\n const read = async <T>(request: () => Promise<T>): Promise<T> => {\n if (!isCurrent()) throw new RetiredSurface();\n try {\n const reply = await request();\n if (!isCurrent()) throw new RetiredSurface();\n return reply;\n } catch (error) {\n if (!isCurrent()) throw new RetiredSurface();\n throw error;\n }\n };\n return { pageId, table, key, read, isCurrent };\n }, [owner, entry, entry.pageId, entry.table, entry.rootBody, keyText]);\n const current = target.isCurrent();\n const definition = useQuery<RecordSurfacePage>({\n queryKey: ['page-def', owner.connection, target.pageId, target.table, owner.queryScope],\n queryFn: () => target.read(() => pageById(target.pageId, target.table) as Promise<RecordSurfacePage>),\n enabled: current,\n staleTime: 0,\n gcTime: 0,\n });\n const page = current ? definition.data : undefined;\n const base = page && !page.error ? (findTable(page.base) as TableMeta | undefined) : undefined;\n /* A suggested page is its table's own: the base and the id are in its name,\n so its root and its plan are asked beside its definition, not after it —\n one round trip off every record a page was never configured for. The\n content still waits for the definition. */\n const suggested =\n !page && target.pageId?.startsWith('suggested:')\n ? (findTable(target.pageId.slice('suggested:'.length)) as TableMeta | undefined)\n : undefined;\n const readBase = base ?? suggested;\n const readPageId = page?.id ?? (suggested ? target.pageId : undefined);\n const needsResolution = Boolean(readBase && (entry.table !== readBase.id || (!entry.key && entry.rootBody !== undefined)));\n let rootError: string | undefined;\n if (options.explicitRoot && readBase) {\n try {\n const checked = recordKey(target.key),\n pk = primaryKey(readBase) as string[];\n if (\n entry.table !== readBase.id ||\n pk.length !== Object.keys(checked).length ||\n pk.some((column) => !Object.hasOwn(checked, column))\n ) {\n throw new Error(say('incompleteSurfaceRoot'));\n }\n } catch {\n rootError = say('thisSurfaceNeedsAComplete');\n }\n }\n const others = useQuery<RecordSurfaceAlternative[]>({\n queryKey: ['pages-of', owner.connection, readBase?.id, owner.queryScope],\n queryFn: () =>\n target.read(async () => {\n try {\n return ((await pagesFor(readBase!.id)) as Array<RecordSurfaceAlternative & { error?: string }>).filter((p) => !p.error);\n } catch {\n return [];\n } // Alternatives failing never removes the known layout.\n }),\n enabled: current && Boolean(readBase) && !needsResolution && !rootError,\n staleTime: 0,\n gcTime: 0,\n });\n const plan = useQuery<{ page?: PageDef; plans?: SectionPlan[] }>({\n queryKey: ['page-plan', owner.connection, readPageId, keyText, page?.source === 'saved' ? JSON.stringify(page) : '', owner.queryScope],\n /* The plan and every read the page's sections will ask, in one request\n (shared/recordReads.ts), or the one a press on the row already began.\n Asked before the probe, which then waits for the row the compose reads. */\n queryFn: () =>\n target.read(() => composeRecordPage(owner.connection, page?.source === 'saved' ? page : readPageId!, target.key!, entry.tab)),\n /* Planning independently attests the root on the scoped server adapter.\n Start it beside the probe: the probe still decides whether this surface\n may render, but no longer puts a round trip ahead of every section. */\n enabled: current && !needsResolution && !rootError && Boolean(readBase && readPageId && target.key),\n staleTime: 0,\n gcTime: 0,\n // The record on screen: a tab coming back re-reads it, however recent.\n meta: FOCUS_ALWAYS,\n });\n const probe = useQuery<{ row: Record<string, unknown> | null; unreachable?: boolean }>({\n queryKey: ['page-root', owner.connection, readPageId, keyText, owner.queryScope],\n queryFn: () =>\n target.read(async () => {\n try {\n const namePath = labelPath(readBase!) as string | undefined;\n const pk = primaryKey(readBase!) as string[];\n const reply = await api('/api/view/run', {\n ...ownedBy(owner.connection),\n count: 'none',\n view: {\n id: `page:${readPageId!}:root`,\n name: readBase!.name,\n base: readBase!.id,\n columns: [...pk, ...(namePath ? [namePath] : [])].map((path) => ({ path })),\n filter: pathEqualityFilter(Object.fromEntries(pk.map((column) => [column, target.key![column]]))),\n limit: 1,\n },\n });\n return { row: reply.rows?.[0] ?? null };\n } catch {\n // Preserve the routed behavior: an unreachable probe lets the planner\n // explain the failure, rather than asserting that the record is gone.\n return { row: null, unreachable: true };\n }\n }),\n // The root probe needs the resolved page/base/key only. Alternatives feed\n // the chooser in the eventual plan, so making this read wait for them\n // serializes two independent requests and delays the missing-root fence.\n enabled: current && !needsResolution && !rootError && Boolean(readBase && readPageId && target.key),\n staleTime: 0,\n gcTime: 0,\n });\n const gone = Boolean(current && probe.data && !probe.data.row && !probe.data.unreachable);\n /* One page object per plan: PageView keys its label ownership on the page\n it was given, and a modal host re-renders on every label it publishes,\n so a fresh spread here would republish forever once an aside is in play. */\n const presented = useMemo(\n () => (page && plan.data ? { ...(plan.data.page ?? page), source: plan.data.page?.source ?? page.source } : undefined),\n [page, plan.data],\n );\n let content: RecordSurfaceContent | undefined;\n if (\n current &&\n !needsResolution &&\n !rootError &&\n base &&\n page &&\n others.data &&\n target.key &&\n probe.data &&\n !gone &&\n plan.data &&\n presented\n ) {\n /* A record page offers every one of its resource's actions, whichever list it was opened from. */\n const declared = shell().inApp() ? resourceOf(shell().app(), base.id)?.actions : undefined;\n const actions: PageProps['actions'] = declared?.length\n ? {\n list: declared,\n queryScope: owner.queryScope,\n matches: async (when) => {\n if (!target.isCurrent()) return false;\n const matches = await rootMatches(base, target.key!, when, owner.connection);\n return target.isCurrent() && matches;\n },\n canUpdate: writeAllowed(base.id),\n grantedCommands: shell().app()?.grantedCommands ?? [],\n grantedActions: grantedActionIds(shell().app(), base.id),\n }\n : undefined;\n content = { entry, page: presented, base, plans: plan.data.plans ?? [], others: others.data, actions };\n }\n return {\n definition,\n page,\n base,\n needsResolution,\n rootError,\n others,\n probe,\n plan,\n gone,\n content,\n refreshFailed: definition.isError || plan.isError,\n rootRow: current ? probe.data?.row : undefined,\n isCurrent: target.isCurrent,\n };\n}\n","/**\n * Custom components, as the App definition declares them: an author's own\n * React module, referenced by path and placed only through a declared kind —\n * a `custom` section on a page or a dashboard, a value renderer where a\n * display rule stands, an input where a form draws a field, a list view's\n * rows (`customList`) or a free-form page (`definePage` in `pages/*.page.tsx`).\n *\n * The definition stays data. `app.ts` is evaluated in a worker and crosses\n * back by structured clone, so a component can never be embedded as a\n * function: `component(id, { module })` is a name and a path, and the server\n * compiles the module beside the App (packages/mistri/src/server/app/appComponents.ts). What\n * the component reads is declared here too — the record's own fields and\n * named reads of rows — so it is checked, planned and authorized like every\n * built-in section, and the module only draws what it is handed.\n *\n * Author code is deployment code, trusted like `commands.ts`; the server\n * stays the only security boundary. A signed-out visitor may meet one where\n * the public grant already lets them read — a page, a list or a value over a\n * resource `auth.public` opens — and every read it makes is the public\n * grant's, refused on the server like any other; what a component declares\n * it reads there is held to that grant as the App loads, so a public page\n * never breaks for its visitors. The landing and public forms stay a closed\n * set of kinds that never runs markup: no component stands on either.\n */\nimport { allowKeys, isRecord } from './model.js';\n\nexport const COMPONENT_KINDS = ['section', 'value', 'input', 'list', 'page'] as const;\nexport type ComponentKind = (typeof COMPONENT_KINDS)[number];\nexport const COMPONENT_PROP_TYPES = ['number', 'text', 'boolean', 'choice'] as const;\n\n/** One declared prop: JSON-safe, with its default and its bounds, so a value is checked at load. */\nexport type ComponentPropDecl =\n | { type: 'number'; default?: number; min?: number; max?: number }\n | { type: 'text'; default?: string; max?: number }\n | { type: 'boolean'; default?: boolean }\n | { type: 'choice'; options: string[]; default?: string };\n\n/** What `component()` returns: a name, a path relative to the App directory, a kind and its props. */\nexport interface ComponentDecl {\n id: string;\n module: string;\n kind: ComponentKind;\n props?: Record<string, ComponentPropDecl>;\n}\n\n/** The key a component is referenced by: lowercase, the file names it is served under start with it. */\nexport const COMPONENT_ID = /^[a-z][a-z0-9-]{0,47}$/;\n/** What a component module may be: TypeScript or JavaScript, with or without JSX. */\nconst COMPONENT_EXTENSIONS = ['.tsx', '.ts', '.jsx', '.js'] as const;\n/**\n * How many named reads one custom section may declare. Its reads are asked together, one request each, so this\n * bounds the fan-out of a single paint (the server runs one active connection per process); the section draws one\n * skeleton until all are in, so more reads never shift the layout. Four was short of a real section (a list, its\n * neighbours and a few tables to look names up in); eight keeps a paint at a handful of queries.\n */\nexport const MAX_CUSTOM_READS = 8;\n/** The keys `component()` writes. */\nconst COMPONENT_KEYS = ['id', 'module', 'kind', 'props'] as const;\n/** How many columns a custom list presentation reads beside the keys. */\nconst MAX_CUSTOM_LIST_SHOW = 32;\n/**\n * How many aggregate reads (`figures`) one custom section may declare. Each is one breakdown, asked with the paint's\n * other breakdowns in a single request (breakdownBatch.ts), so the bound is on the work one section adds to it.\n */\nexport const MAX_CUSTOM_FIGURES = 8;\n/** The keys a declared read of rows takes, on a page and on a dashboard. */\nexport const PAGE_READ_KEYS = ['from', 'whose', 'query', 'show', 'sort', 'limit'] as const;\nexport const DASH_READ_KEYS = ['from', 'query', 'show', 'sort', 'limit', 'over'] as const;\n/** The keys a declared aggregate read takes, on a page and on a dashboard. */\nconst PAGE_TOTAL_KEYS = ['from', 'measure', 'by', 'whose', 'query', 'limit'] as const;\nconst DASH_TOTAL_KEYS = ['from', 'measure', 'by', 'query', 'over', 'limit'] as const;\n/** What a total measures: the breakdown grammar's aggregates over one column of the resource (count takes none). */\nconst TOTAL_FUNCTIONS = ['count', 'sum', 'avg', 'min', 'max'] as const;\ntype TotalFunction = (typeof TOTAL_FUNCTIONS)[number];\nconst TOTAL_MEASURE = /^(count|(?:sum|avg|min|max)\\s+[A-Za-z_][A-Za-z0-9_]*)$/;\n/** The most groups one total returns, and what it returns when it says none. */\nconst MAX_TOTAL_GROUPS = 500;\nconst DEFAULT_TOTAL_GROUPS = 100;\n\nconst PATH = /^[A-Za-z_][A-Za-z0-9_]*(\\.[A-Za-z_][A-Za-z0-9_]*)*$/;\nconst NAME = /^[A-Za-z][A-Za-z0-9_]{0,39}$/;\n\n/** Whether a value is a component declaration: what a display rule or a section names in place of its own words. */\nexport function isComponentDecl(value: unknown): value is Record<string, unknown> {\n return isRecord(value) && typeof value.module === 'string' && typeof value.id === 'string';\n}\n\n/**\n * A component declaration, checked in shape. The file itself — that it\n * exists, inside the App directory, and compiles — is the loader's to judge\n * (it knows the directory); this is what can be said of the data alone.\n */\nexport function parseComponentDecl(raw: unknown, at: string, expected?: ComponentKind): ComponentDecl {\n if (!isRecord(raw)) throw new Error(`${at} is not a component — declare it with component('id', { module: './widgets/Thing.tsx' }).`);\n allowKeys(raw, COMPONENT_KEYS, `${at} (a component)`);\n const id = raw.id;\n if (typeof id !== 'string' || !COMPONENT_ID.test(id)) {\n throw new Error(\n `${at} has a component id ${JSON.stringify(id)}: lowercase letters, digits and hyphens, starting with a letter, at most 48 characters.`,\n );\n }\n const module = raw.module;\n if (\n typeof module !== 'string' ||\n !module.startsWith('./') ||\n module.includes('\\\\') ||\n module.split('/').some((part) => part === '..' || part === '') ||\n !COMPONENT_EXTENSIONS.some((ext) => module.endsWith(ext))\n ) {\n throw new Error(\n `${at}: the component \"${id}\" names the module ${JSON.stringify(module)}. A module is a path inside the App directory, ` +\n `written from it — './widgets/Thing.tsx' — ending in ${COMPONENT_EXTENSIONS.join(', ')}.`,\n );\n }\n const kind = raw.kind === undefined ? 'section' : raw.kind;\n if (!(COMPONENT_KINDS as readonly unknown[]).includes(kind)) {\n throw new Error(\n `${at}: the component \"${id}\" has a kind of ${JSON.stringify(kind)}. Use ${COMPONENT_KINDS.map((k) => `\"${k}\"`).join(' or ')}.`,\n );\n }\n if (expected && kind !== expected) {\n throw new Error(\n `${at} names the component \"${id}\", ${KIND_WORDS[kind as ComponentKind]}. ${PLACED[expected]} takes a component of kind \"${expected}\": ` +\n `component('${id}', { module, kind: '${expected}' }).`,\n );\n }\n const props = raw.props === undefined ? undefined : parsePropDecls(raw.props, `${at}: the component \"${id}\"`);\n return { id, module, kind: kind as ComponentKind, ...(props ? { props } : {}) };\n}\n\n/** What each kind is, and where each is placed, for a refusal that says both. */\nconst KIND_WORDS: Record<ComponentKind, string> = {\n section: 'a section',\n value: 'a value renderer',\n input: 'an input',\n list: 'a list presentation',\n page: 'a free-form page',\n};\nconst PLACED: Record<ComponentKind, string> = {\n section: 'A custom section',\n value: 'A display rule',\n input: 'An input',\n list: 'customList()',\n page: 'A free-form page (pages/*.page.tsx)',\n};\n\nfunction parsePropDecls(raw: unknown, at: string): Record<string, ComponentPropDecl> {\n if (!isRecord(raw))\n throw new Error(`${at} has \"props\" that are not an object of prop.number(), prop.text(), prop.bool() or prop.choice().`);\n const out: Record<string, ComponentPropDecl> = {};\n for (const [name, entry] of Object.entries(raw)) {\n const here = `${at}, prop \"${name}\"`;\n if (!NAME.test(name)) throw new Error(`${here}: a prop's name is a word of letters, digits and underscores.`);\n if (!isRecord(entry) || !(COMPONENT_PROP_TYPES as readonly unknown[]).includes(entry.type)) {\n throw new Error(`${here} is not a prop declaration — write prop.number(), prop.text(), prop.bool() or prop.choice([...]).`);\n }\n const decl = entry as ComponentPropDecl & Record<string, unknown>;\n if (decl.type === 'number') {\n allowKeys(decl, ['type', 'default', 'min', 'max'], here);\n for (const key of ['min', 'max'] as const) {\n if (decl[key] !== undefined && (typeof decl[key] !== 'number' || !Number.isFinite(decl[key])))\n throw new Error(`${here} has a \"${key}\" that is not a number.`);\n }\n if (decl.min !== undefined && decl.max !== undefined && decl.min > decl.max) throw new Error(`${here} has a \"min\" above its \"max\".`);\n } else if (decl.type === 'text') {\n allowKeys(decl, ['type', 'default', 'max'], here);\n if (decl.max !== undefined && (!Number.isInteger(decl.max) || decl.max < 1))\n throw new Error(`${here} has a \"max\" that is not a positive whole number.`);\n } else if (decl.type === 'boolean') {\n allowKeys(decl, ['type', 'default'], here);\n } else {\n allowKeys(decl, ['type', 'options', 'default'], here);\n if (\n !Array.isArray(decl.options) ||\n !decl.options.length ||\n decl.options.some((option) => typeof option !== 'string' || !option.trim())\n ) {\n throw new Error(`${here} is a choice with no \"options\": name its values, prop.choice(['week', 'month']).`);\n }\n }\n /* A default is a value like any other: checked against its own declaration now,\n so a component never receives what its declaration says it cannot. */\n if (decl.default !== undefined) propValue(decl, decl.default, `${here}, its default`);\n out[name] = { ...decl } as ComponentPropDecl;\n }\n return out;\n}\n\nfunction propValue(decl: ComponentPropDecl, value: unknown, at: string): unknown {\n switch (decl.type) {\n case 'number':\n if (typeof value !== 'number' || !Number.isFinite(value)) throw new Error(`${at} is ${JSON.stringify(value)}, not a number.`);\n if (decl.min !== undefined && value < decl.min) throw new Error(`${at} is ${value}, below its min of ${decl.min}.`);\n if (decl.max !== undefined && value > decl.max) throw new Error(`${at} is ${value}, above its max of ${decl.max}.`);\n return value;\n case 'text':\n if (typeof value !== 'string') throw new Error(`${at} is ${JSON.stringify(value)}, not text.`);\n if (decl.max !== undefined && value.length > decl.max) throw new Error(`${at} is longer than its max of ${decl.max} characters.`);\n return value;\n case 'boolean':\n if (typeof value !== 'boolean') throw new Error(`${at} is ${JSON.stringify(value)}, not true or false.`);\n return value;\n case 'choice':\n if (typeof value !== 'string' || !decl.options.includes(value))\n throw new Error(`${at} is ${JSON.stringify(value)}; it is one of ${decl.options.map((o) => `\"${o}\"`).join(', ')}.`);\n return value;\n }\n}\n\n/**\n * The props a placement hands its component: each value checked against the\n * component's declaration and every default filled in, so the module reads\n * exactly the shape its types promise. A prop the declaration does not name is\n * refused, as an unknown key is everywhere else.\n */\nexport function componentPropValues(decl: ComponentDecl, raw: unknown, at: string): Record<string, unknown> {\n if (raw !== undefined && !isRecord(raw)) throw new Error(`${at} has \"props\" that are not an object.`);\n const given = (raw ?? {}) as Record<string, unknown>;\n const declared = decl.props ?? {};\n for (const name of Object.keys(given)) {\n if (!Object.hasOwn(declared, name)) {\n const names = Object.keys(declared);\n throw new Error(\n `${at} gives the prop \"${name}\", which the component \"${decl.id}\" does not declare. ${names.length ? `It declares: ${names.join(', ')}.` : 'It declares none.'}`,\n );\n }\n }\n const out: Record<string, unknown> = {};\n for (const [name, prop] of Object.entries(declared)) {\n const value = Object.hasOwn(given, name) ? given[name] : prop.default;\n if (value !== undefined) out[name] = propValue(prop, value, `${at}, prop \"${name}\"`);\n }\n return out;\n}\n\n/** One declared read of rows on a page: related to the record by `whose`, or the resource's own rows, narrowed. */\nexport interface PageCustomRead {\n from: string;\n /** Config `whose`: the path from `from` back to the page's record. Absent, the rows are not tied to it. */\n path?: string;\n filter?: string;\n columns: string[];\n /** Query-bar text, as authored; `orderBy` is what the planner runs. */\n sort?: string;\n orderBy?: Array<{ column: string; direction: 'asc' | 'desc' }>;\n limit?: number;\n}\n\n/** One declared read of rows on a dashboard: the rows section's words, the window's `over` included. */\nexport interface DashCustomRead {\n table: string;\n query?: string;\n show: string[];\n sort?: string;\n limit?: number;\n over?: string;\n /** Served, never authored: the query without its own window, as a rows section's. */\n windowBase?: string;\n}\n\nfunction readShape(raw: unknown, at: string, keys: readonly string[]): Record<string, unknown> {\n if (!isRecord(raw)) throw new Error(`${at} is not an object like { from: 'invoice', show: ['number', 'total'] }.`);\n allowKeys(raw, keys, at);\n if (typeof raw.from !== 'string' || !raw.from.trim()) throw new Error(`${at} has no \"from\": the resource its rows come from.`);\n if (!Array.isArray(raw.show) || !raw.show.length || raw.show.some((path) => typeof path !== 'string' || !PATH.test(path.trim()))) {\n throw new Error(`${at} has no \"show\": the columns the component reads, label paths welcome — ['number', 'customer_id.name'].`);\n }\n for (const key of ['query', 'sort', 'whose', 'over'] as const) {\n if (raw[key] !== undefined && (typeof raw[key] !== 'string' || !(raw[key] as string).trim()))\n throw new Error(`${at} has a \"${key}\" that is not a non-empty string.`);\n }\n if (raw.limit !== undefined && (typeof raw.limit !== 'number' || !Number.isInteger(raw.limit) || raw.limit < 1 || raw.limit > 500)) {\n throw new Error(`${at} has a \"limit\" that is not a whole number from 1 to 500.`);\n }\n return raw;\n}\n\nfunction reads<T>(\n raw: unknown,\n at: string,\n one: (entry: unknown, here: string) => T,\n { key = 'rows', most = MAX_CUSTOM_READS, noun = 'reads' }: { key?: string; most?: number; noun?: string } = {},\n): Record<string, T> | undefined {\n if (raw === undefined) return undefined;\n if (!isRecord(raw) || !Object.keys(raw).length)\n throw new Error(`${at} has \"${key}\" that are not an object of named ${noun}: { runs: { from, show } }.`);\n const names = Object.keys(raw);\n if (names.length > most) throw new Error(`${at} declares ${names.length} ${noun}; a custom section reads at most ${most}.`);\n return Object.fromEntries(\n names.map((name) => {\n if (!NAME.test(name))\n throw new Error(`${at}: a read's name is a word of letters, digits and underscores, like \"runs\"; \"${name}\" is not.`);\n return [name, one(raw[name], `${at}, ${key} \"${name}\"`)];\n }),\n );\n}\n\nexport function parsePageReads(raw: unknown, at: string): Record<string, PageCustomRead> | undefined {\n return reads(raw, at, (entry, here) => {\n const r = readShape(entry, here, PAGE_READ_KEYS);\n return {\n from: (r.from as string).trim(),\n ...(r.whose === undefined ? {} : { path: (r.whose as string).trim() }),\n ...(r.query === undefined ? {} : { filter: (r.query as string).trim() }),\n columns: (r.show as string[]).map((path) => path.trim()),\n ...(r.sort === undefined ? {} : { sort: (r.sort as string).trim() }),\n ...(r.limit === undefined ? {} : { limit: r.limit as number }),\n };\n });\n}\n\nexport function parseDashReads(raw: unknown, at: string): Record<string, DashCustomRead> | undefined {\n return reads(raw, at, (entry, here) => {\n const r = readShape(entry, here, DASH_READ_KEYS);\n return {\n table: (r.from as string).trim(),\n ...(r.query === undefined ? {} : { query: (r.query as string).trim() }),\n show: (r.show as string[]).map((path) => path.trim()),\n ...(r.sort === undefined ? {} : { sort: (r.sort as string).trim() }),\n ...(r.limit === undefined ? {} : { limit: r.limit as number }),\n ...(r.over === undefined ? {} : { over: (r.over as string).trim() }),\n };\n });\n}\n\n/** One declared aggregate read on a page: a count, sum, average, minimum or maximum, tied to the record by `whose`. */\nexport interface PageCustomTotal {\n from: string;\n fn: TotalFunction;\n /** The column measured; absent for a count. */\n column?: string;\n /** What the figure is grouped by, in the breakdown grammar (\"status\", \"month opened_on\"); absent, one figure. */\n by?: string;\n /** Config `whose`: the path from `from` back to the page's record. Absent, the resource's own rows. */\n path?: string;\n filter?: string;\n limit?: number;\n}\n\n/** One declared aggregate read on a dashboard: a measure in the breakdown grammar, narrowed by the window's `over`. */\nexport interface DashCustomTotal {\n table: string;\n /** \"count\" or \"sum amount\": the breakdown grammar's measure. */\n measure: string;\n by?: string;\n query?: string;\n over?: string;\n limit?: number;\n /** Served, never authored: the query without its own window, as a rows section's. */\n windowBase?: string;\n}\n\nfunction totalShape(raw: unknown, at: string, keys: readonly string[]): Record<string, unknown> & { measure: string } {\n if (!isRecord(raw)) throw new Error(`${at} is not an object like { from: 'invoice', measure: 'sum total' }.`);\n allowKeys(raw, keys, at);\n if (typeof raw.from !== 'string' || !raw.from.trim()) throw new Error(`${at} has no \"from\": the resource it measures.`);\n if (typeof raw.measure !== 'string' || !TOTAL_MEASURE.test(raw.measure.trim().replace(/\\s+/g, ' '))) {\n throw new Error(`${at} has no \"measure\": \"count\", or ${TOTAL_FUNCTIONS.slice(1).join(', ')} and a column — 'sum amount'.`);\n }\n for (const key of ['by', 'query', 'whose', 'over'] as const) {\n if (raw[key] !== undefined && (typeof raw[key] !== 'string' || !(raw[key] as string).trim()))\n throw new Error(`${at} has a \"${key}\" that is not a non-empty string.`);\n }\n if (raw.limit !== undefined) {\n if (raw.by === undefined) throw new Error(`${at} has a \"limit\" and no \"by\": a figure with no groups has no groups to cut.`);\n if (typeof raw.limit !== 'number' || !Number.isInteger(raw.limit) || raw.limit < 1 || raw.limit > MAX_TOTAL_GROUPS)\n throw new Error(`${at} has a \"limit\" that is not a whole number from 1 to ${MAX_TOTAL_GROUPS}.`);\n }\n return { ...raw, measure: raw.measure.trim().replace(/\\s+/g, ' ') };\n}\n\nconst totalReads = { key: 'figures', most: MAX_CUSTOM_FIGURES, noun: 'figures' };\n\nexport function parsePageTotals(raw: unknown, at: string): Record<string, PageCustomTotal> | undefined {\n return reads(\n raw,\n at,\n (entry, here) => {\n const r = totalShape(entry, here, PAGE_TOTAL_KEYS);\n const [fn, column] = r.measure.split(' ') as [TotalFunction, string | undefined];\n return {\n from: (r.from as string).trim(),\n fn,\n ...(column === undefined ? {} : { column }),\n ...(r.by === undefined ? {} : { by: (r.by as string).trim() }),\n ...(r.whose === undefined ? {} : { path: (r.whose as string).trim() }),\n ...(r.query === undefined ? {} : { filter: (r.query as string).trim() }),\n ...(r.limit === undefined ? {} : { limit: r.limit as number }),\n };\n },\n totalReads,\n );\n}\n\nexport function parseDashTotals(raw: unknown, at: string): Record<string, DashCustomTotal> | undefined {\n return reads(\n raw,\n at,\n (entry, here) => {\n const r = totalShape(entry, here, DASH_TOTAL_KEYS);\n return {\n table: (r.from as string).trim(),\n measure: r.measure,\n ...(r.by === undefined ? {} : { by: (r.by as string).trim() }),\n ...(r.query === undefined ? {} : { query: (r.query as string).trim() }),\n ...(r.over === undefined ? {} : { over: (r.over as string).trim() }),\n ...(r.limit === undefined ? {} : { limit: r.limit as number }),\n };\n },\n totalReads,\n );\n}\n\n/** How many groups a total with a `by` returns when it says no limit. */\nexport const groupsAsked = (limit: number | undefined): number => limit ?? DEFAULT_TOTAL_GROUPS;\n\n/** A record's own fields a page's custom section reads. */\nexport function parseRecordFields(raw: unknown, at: string): string[] | undefined {\n if (raw === undefined) return undefined;\n if (!Array.isArray(raw) || !raw.length || raw.some((path) => typeof path !== 'string' || !PATH.test(path.trim()))) {\n throw new Error(\n `${at} has a \"record\" that is not a list of the record's fields, label paths welcome — ['status', 'customer_id.name'].`,\n );\n }\n return raw.map((path) => (path as string).trim());\n}\n\n/** A custom list presentation as the App serves it: the component, the columns it reads, its props. */\nexport interface CustomListPresentation {\n kind: 'custom';\n component: ComponentDecl;\n show: string[];\n props: Record<string, unknown>;\n}\n\n/**\n * `customList(postStream, { show: ['body', 'author_id.display_name'] })`: a\n * list view's rows drawn by the author's component. The platform keeps the\n * query, the tabs, the filters, the sort, the paging and the bulk actions;\n * `show` is what the query reads beside the keys, so every row the component\n * receives carries exactly those columns.\n */\nexport function parseCustomList(raw: Record<string, unknown>, at: string): CustomListPresentation {\n allowKeys(raw, ['kind', 'component', 'show', 'props'], `${at} (a custom list)`);\n const component = parseComponentDecl(raw.component, `${at}, \"component\"`, 'list');\n const show = raw.show;\n if (\n !Array.isArray(show) ||\n !show.length ||\n show.length > MAX_CUSTOM_LIST_SHOW ||\n show.some((path) => typeof path !== 'string' || !PATH.test(path.trim()))\n ) {\n throw new Error(\n `${at} has no \"show\": the columns its component reads, one to ${MAX_CUSTOM_LIST_SHOW}, label paths welcome — ['body', 'author_id.display_name'].`,\n );\n }\n const columns = show.map((path) => (path as string).trim());\n if (new Set(columns).size !== columns.length) throw new Error(`${at} names one column twice in \"show\".`);\n return { kind: 'custom', component, show: columns, props: componentPropValues(component, raw.props, at) };\n}\n\n/** A free-form page as its `pages/*.page.tsx` file declares it, before it is served as the dashboard that draws it (appViews.ts). */\ninterface PageDecl {\n component: ComponentDecl;\n /** The resources the page reads, which gate it: a reader who cannot read one of them is not offered it. */\n reads: string[];\n /** The commands the page runs; any other is refused, in the browser and by the server. */\n commands: string[];\n /** The address parameters the page keeps its place in: `/{app}/pick-walk?list=12`. */\n params: string[];\n /** The loader that reads for it on the server, by its name in the commands module (`defineLoaders`). */\n loader?: string;\n props: Record<string, unknown>;\n}\n\n/** The most address parameters one page keeps. */\nconst MAX_PAGE_PARAMS = 8;\nconst PARAM = /^[a-z][a-z0-9_]{0,31}$/;\nconst LOADER = /^[A-Za-z][A-Za-z0-9_]{0,63}$/;\n\nconst words = (raw: unknown): raw is string[] => Array.isArray(raw) && raw.every((name) => typeof name === 'string' && name.trim() !== '');\n\n/**\n * `definePage({ reads: ['pick_list'], commands: ['pickList'], params: ['list'], component })`:\n * a whole page drawn by the author's component. What it reads and runs is\n * declared, so a reader who cannot read every one of its resources is not\n * offered it, `mistri check` holds the module to it, and authority.lock\n * records it; its place is kept in the address by the parameters it names,\n * so a page reached by link opens where it was. A bracketed code leads each\n * refusal, for `mistri check` (checkProblems.ts).\n */\nexport function parsePage(raw: Record<string, unknown>, at: string): PageDecl {\n const component = parseComponentDecl(raw.component, `${at}, \"component\"`, 'page');\n const reads = raw.reads;\n if (!words(reads) || !reads.length) {\n throw new Error(\n `[page-reads] ${at} has no \"reads\": the resources the page reads — reads: ['pick_list', 'order_line']. A page reads nothing it does not declare.`,\n );\n }\n const commands = raw.commands === undefined ? [] : raw.commands;\n if (!words(commands))\n throw new Error(`[page-commands] ${at} has \"commands\" that are not a list of command names: commands: ['pickList'].`);\n const params = raw.params === undefined ? [] : raw.params;\n if (\n !Array.isArray(params) ||\n params.length > MAX_PAGE_PARAMS ||\n params.some((name) => typeof name !== 'string' || !PARAM.test(name)) ||\n params.includes('period')\n ) {\n throw new Error(\n `[page-params] ${at} has \"params\" that are not up to ${MAX_PAGE_PARAMS} lowercase words, like params: ['list']; \"period\" is a dashboard's.`,\n );\n }\n if (raw.loader !== undefined && (typeof raw.loader !== 'string' || !LOADER.test(raw.loader))) {\n throw new Error(\n `[page-loader] ${at} has a \"loader\" that is not a loader's name, as the commands module's defineLoaders() registers it.`,\n );\n }\n return {\n component,\n reads: [...new Set(reads.map((name) => name.trim()))],\n commands: [...new Set(commands.map((name) => name.trim()))],\n params: [...new Set(params as string[])],\n ...(raw.loader === undefined ? {} : { loader: raw.loader as string }),\n props: componentPropValues(component, raw.props, at),\n };\n}\n\n/**\n * `inputs: { color: colorPicker }`: a component standing in a form's field —\n * a resource's column in its edit and New forms, or a command's input in an\n * action's or a signed-in form's command form. It draws the control and\n * hands back a value; what the value must be is still the form's rules and\n * the server's to judge.\n */\nexport function parseInputs(raw: unknown, at: string, options: { paths?: boolean } = {}): Record<string, ComponentDecl> | undefined {\n if (raw === undefined) return undefined;\n if (!isRecord(raw) || !Object.keys(raw).length)\n throw new Error(`${at} has \"inputs\" that are not an object of field → input component: { color: colorPicker }.`);\n /* A command's input inside a group is named by its path, `trade.vin`: in a repeating group, every row's. */\n const named = options.paths ? /^[A-Za-z_][A-Za-z0-9_]*(?:\\.[A-Za-z_][A-Za-z0-9_]*)*$/ : /^[A-Za-z_][A-Za-z0-9_]*$/;\n return Object.fromEntries(\n Object.entries(raw).map(([name, decl]) => {\n if (!named.test(name))\n throw new Error(`${at}, \"inputs\": \"${name}\" is not a field's name${options.paths ? ' or a path into a group (trade.vin)' : ''}.`);\n return [name, parseComponentDecl(decl, `${at}, inputs \"${name}\"`, 'input')];\n }),\n );\n}\n\n/** A command's input by its name or its path through groups (`trade.vin`), or undefined; a group itself is not one, nor are lines (their grid is the platform's). */\nexport function commandInputAt<F extends { name: string; type?: string; fields?: readonly F[] }>(\n fields: readonly F[],\n path: string,\n): F | undefined {\n const [head, ...rest] = path.split('.');\n const field = fields.find((candidate) => candidate.name === head);\n if (!field) return undefined;\n if (!rest.length) return field.type === 'group' || field.type === 'lines' ? undefined : field;\n return field.type === 'group' && field.fields ? commandInputAt(field.fields, rest.join('.')) : undefined;\n}\n","/*\n * Reading pages from configuration: the `pages` section of a tablewalk.json\n * and an App's page bodies. A malformed page throws with the page and, where\n * there is one, the fix; nothing is skipped.\n */\nimport { isMessageRef, parseCaption } from './appMessages.js';\nimport { parseTableSizing } from './tableSizing.js';\nimport { parseRelatedCreate } from './relatedCreate.js';\nimport { parseListEmpty } from './listEmpty.js';\nimport { CHART_SHAPES, FIGURE_FORMATS, FIGURE_FORMAT_WORDS, allowKeys, humanName } from './model.js';\nimport { headingOf } from './appNoun.js';\nimport { CARDS_MAX_LINES, parseCards, parseFacts, type AppCardsList } from './appCards.js';\nimport { parseIcon } from './appIcons.js';\nimport { slug } from './slug.js';\nimport { parsePageLayout } from './pageLayouts.js';\nimport { parseSortText } from './sortText.js';\nimport { componentPropValues, parseComponentDecl, parsePageReads, parsePageTotals, parseRecordFields } from './appComponents.js';\nimport { parseAppLinkAction, parseAppTarget, type AppLinkAction } from './appLinks.js';\nimport {\n type PageChart,\n type SectionWidth,\n WIDTHS,\n type SectionOffset,\n OFFSETS,\n type FieldColumns,\n type SectionRegion,\n SECTION_REGIONS,\n FIELD_COLUMNS,\n type PageMetric,\n type PageList,\n MAX_STREAMS,\n MAX_HISTORY_LIMIT,\n type TimelineComposer,\n type TimelineTone,\n type PageSection,\n type PageDef,\n MAX_SECTIONS,\n MAX_PAGE_SECTIONS,\n screenSections,\n type PageHeadlineAction,\n type PageSimilar,\n type PageGroup,\n GROUPS_MOST,\n GROUP_ROWS_MOST,\n} from './pageModel.js';\n\n/* ---------- config ---------- */\n\n/**\n * Read the `pages` section of a tablewalk.json.\n *\n * Throws on a malformed file rather than skipping the bad entry: a page that\n * silently did not appear is a bug report that starts \"I definitely wrote\n * it\". Everything a config file can get wrong is named with the page it is\n * in and, where there is one, the fix.\n */\nexport function parsePagesConfig(raw: unknown, where: string): PageDef[] {\n const pages = (raw as { pages?: unknown })?.pages;\n if (pages === undefined) return [];\n if (!Array.isArray(pages)) throw new Error(`${where}: \"pages\" must be an array.`);\n\n const seen = new Set<string>();\n return pages.map((entry, i) => {\n const at = `${where}: page ${i + 1}`;\n if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {\n throw new Error(`${at} is not an object.`);\n }\n const p = entry as Record<string, unknown>;\n const said = typeof p.label === 'string' && p.label.trim() ? ` (\"${p.label.trim()}\")` : '';\n if (p.id !== undefined && (typeof p.id !== 'string' || !p.id.trim())) {\n throw new Error(`${at}${said} has an \"id\" that is not a non-empty string.`);\n }\n const named = p.label !== undefined ? parseCaption(p.label, `${at}.label`) : typeof p.base === 'string' ? humanName(p.base.trim()) : '';\n const id = p.id === undefined ? `config-${slug(named, 'page')}` : String(p.id);\n const page = parsePageBody(p, at, id, true);\n /* An embed and a note's action name an App's views; a tablewalk.json has none to name. */\n for (const [n, section] of page.sections.entries()) {\n if (section.kind === 'list' && section.view !== undefined) {\n throw new Error(\n `${at} (\"${page.name}\") section ${n + 1} embeds the view \"${section.view}\", and a config file has no views: write a related list ({ kind: \"list\", from, whose }) instead.`,\n );\n }\n if (section.kind === 'note' && section.action) {\n throw new Error(\n `${at} (\"${page.name}\") section ${n + 1} is a note whose \"action\" needs an App, and a config file has no views to open. Remove \"action\".`,\n );\n }\n }\n if (seen.has(id)) throw new Error(`${at} (\"${page.name}\") repeats the id \"${id}\".`);\n seen.add(id);\n return page;\n });\n}\n\n/* Spellings renamed before 1.0, refused with the word that replaced them (allowKeys' `refused`). */\n/* A call to action's emphasis is its `intent`, as a button's is; `kind` names what a thing is. */\nconst CUSTOM_RENAMED = {\n totals: 'is now \"figures\": the named aggregate reads its component receives as figures.<name> (\"totals\" is a list\\'s footer of sums)',\n} as const;\nconst INTENT_RENAMED = { kind: 'is now \"intent\", as a button says it: \"primary\" or \"secondary\"' } as const;\nconst PAGE_RENAMED = { onlyOn: 'is now \"connection\", as the root of the config file says it' } as const;\n/** The keys a page takes. A tablewalk.json page also names its `id`; an App page is keyed by it. */\nexport const PAGE_KEYS = ['label', 'base', 'connection', 'listTabs', 'empty', 'sections', 'aside', 'layout'] as const;\n/** An App page has no `connection`: the App names its one connection, and a page scoped to another would never show. Nor a `label`: it is called its resource's `noun`. */\nexport const APP_PAGE_KEYS = PAGE_KEYS.filter((key) => key !== 'connection' && key !== 'label');\n/* `aside` and `layout` are an App page's alone: the Workbench's pages draw what they say, in the record's one skeleton. */\nconst WORKBENCH_PAGE_KEYS = PAGE_KEYS.filter((key) => key !== 'aside' && key !== 'layout');\nconst SECTION_COMMON = ['kind', 'title', 'region', 'tab', 'width', 'offsetLeft', 'offsetRight', 'hidden', 'when'] as const;\n/** A tab's name is a word or two across the strip, not a sentence. */\nconst MAX_TAB_NAME = 40;\n/** The keys each page section kind takes. Anything else is refused by name. A `segment` is read before the\n section is (appSegments.ts unfoldSegments), as its word in `query`. */\n/* `view` is an authored kind with no section of its own: `embed(view, { whose })`\n is read into a related list that remembers the view (PageList.view). */\nexport const PAGE_SECTION_KEYS: Record<PageSection['kind'] | 'view' | 'header' | 'current' | 'card' | 'headline', readonly string[]> = {\n fields: [...SECTION_COMMON, 'show', 'columns', 'names', 'presentation', 'editable'],\n /* The record a reference names, in a box of its own: the first shown field is its title, the rest its facts. */\n card: [...SECTION_COMMON, 'show'],\n /* A figure with its facts, its earlier value, its estimate and the calls to action beside it. */\n headline: [...SECTION_COMMON, 'show', 'links', 'subfigure', 'was', 'note'],\n /* The record's head: drawn where the head is, so nothing that would place it elsewhere. */\n header: ['kind', 'status', 'facts', 'primary', 'destructive', 'actions'],\n /* The one current related row as a card: nothing to page, sum or add under it. */\n current: ['kind', 'title', 'region', 'tab', 'width', 'when', 'from', 'whose', 'show', 'query', 'sort', 'empty'],\n timeline: [...SECTION_COMMON, 'streams', 'limit', 'create', 'newest', 'composer', 'tone', 'compact', 'side'],\n history: [...SECTION_COMMON, 'limit'],\n flows: [...SECTION_COMMON],\n diagram: [...SECTION_COMMON],\n custom: [...SECTION_COMMON, 'component', 'record', 'rows', 'figures', 'props'],\n metric: [...SECTION_COMMON, 'from', 'whose', 'about', 'measure', 'query', 'segment', 'format', 'unit'],\n breakdown: [...SECTION_COMMON, 'from', 'whose', 'about', 'by', 'measure', 'query', 'segment', 'limit', 'format', 'unit'],\n chart: [\n ...SECTION_COMMON,\n 'from',\n 'whose',\n 'about',\n 'by',\n 'measure',\n 'shape',\n 'sort',\n 'query',\n 'segment',\n 'limit',\n 'height',\n 'target',\n 'format',\n 'unit',\n ],\n note: [...SECTION_COMMON, 'text', 'icon', 'action', 'variant'],\n view: [...SECTION_COMMON, 'view', 'whose', 'about', 'limit'],\n list: [\n ...SECTION_COMMON,\n 'from',\n 'whose',\n 'show',\n 'query',\n 'segment',\n 'limit',\n 'sort',\n 'create',\n 'empty',\n 'sizing',\n 'presentation',\n 'totals',\n 'same',\n 'near',\n 'rowActions',\n 'actions',\n 'group',\n ],\n};\nexport const STREAM_KEYS = ['from', 'whose', 'at', 'show', 'detail', 'author', 'query', 'about', 'side', 'tone', 'compact'] as const;\n\n/**\n * One page body: a tablewalk.json page, whose `id` is its own or derives\n * from the name, or an App page, whose id is its key. The body is the same\n * either way, so the page builder, the scaffold and mistri check emit one\n * shape. Every key is checked: an unknown one is refused with the list.\n */\nexport function parsePageBody(\n p: Record<string, unknown>,\n at: string,\n id: string,\n withId = false,\n noun?: string,\n keys: readonly string[] = WORKBENCH_PAGE_KEYS,\n): PageDef {\n const caption = p.label === undefined ? undefined : parseCaption(p.label, `${at}.label`);\n if (typeof p.base !== 'string' || !p.base.trim()) {\n throw new Error(`${at}${typeof p.label === 'string' ? ` (\"${p.label.trim()}\")` : ''} has no \"base\" table.`);\n }\n /* A page is called what its records are: its resource's noun, else its table's name. */\n const name = caption ?? (noun ? headingOf(noun) : humanName(p.base.trim()));\n /* Said by name rather than as an unknown key: an author who copied an App's page into a tablewalk.json is told\n why it does not apply there, not that the word does not exist. */\n if (p.layout !== undefined && !keys.includes('layout')) {\n throw new Error(\n `[page-layout] ${at} (\"${name}\") has \"layout\", which is an App page's: a config file's page draws the record's one skeleton. Remove it.`,\n );\n }\n allowKeys(p, withId ? [...keys, 'id'] : keys, `${at} (\"${name}\")`, keys.includes('connection') ? PAGE_RENAMED : undefined);\n if (p.connection !== undefined && typeof p.connection !== 'string') {\n throw new Error(`${at} (\"${name}\") has a \"connection\" that is not a string.`);\n }\n /* Refused rather than coerced, like every other boolean in this file:\n `\"listTabs\": \"false\"` is a truthy string. */\n if (p.listTabs !== undefined && typeof p.listTabs !== 'boolean') {\n throw new Error(`${at} (\"${name}\") has \"listTabs\": ${JSON.stringify(p.listTabs)}. It must be true or false, without quotes.`);\n }\n /* The only value is the opt-out: a default aside is not something an author switches on. */\n if (p.aside !== undefined && p.aside !== false) {\n throw new Error(`${at} (\"${name}\") has \"aside\": ${JSON.stringify(p.aside)}. It can only be false, to leave the default aside out.`);\n }\n /* What every related list that says nothing of its own does while it has no rows. A list's own `empty` wins. */\n if (p.empty !== undefined && p.empty !== 'hide' && p.empty !== 'collapse') {\n throw new Error(\n `${at} (\"${name}\") has \"empty\": ${JSON.stringify(p.empty)}. A page's is 'hide' (leave an empty list out) or 'collapse' (say so in one line); a list takes guidance of its own.`,\n );\n }\n const pageEmpty = p.empty as 'hide' | 'collapse' | undefined;\n const sections = parseSections(p.sections, at, name, keys.includes('connection')).map((section) =>\n pageEmpty && section.kind === 'list' && section.presentation !== 'card' && !section.empty && !section.emptyMode\n ? { ...section, emptyMode: pageEmpty }\n : section,\n );\n return {\n id,\n name,\n base: p.base.trim(),\n sections,\n connection: p.connection as string | undefined,\n /* Stored as written: an App's page draws a tab strip of its lists only\n when it says `true`, and the Workbench's only unless it says `false`. */\n ...(typeof p.listTabs === 'boolean' ? { listTabs: p.listTabs } : {}),\n ...(pageEmpty ? { empty: pageEmpty } : {}),\n ...(p.aside === false ? { aside: false as const } : {}),\n ...(p.layout !== undefined ? { layout: parsePageLayout(p.layout, `${at} (\"${name}\")`) } : {}),\n source: 'config' as const,\n };\n}\n\nfunction parseSections(raw: unknown, at: string, name: string, config: boolean): PageSection[] {\n if (raw === undefined) return [{ kind: 'fields', width: 'full' }];\n if (!Array.isArray(raw)) throw new Error(`${at} (\"${name}\") has \"sections\" that are not an array.`);\n if (raw.length > MAX_PAGE_SECTIONS) {\n throw new Error(`${at} (\"${name}\") has more than ${MAX_PAGE_SECTIONS} sections, its tabs' included.`);\n }\n const sections = raw.map((entry, i) => parseSection(entry, `${at} (\"${name}\") section ${i + 1}`, config));\n /* A page is a screen, not a report: what one screen draws is counted, so a record that holds more says it in\n tabs, each a screen of its own (a major incident's comms log, bridge and review beside the ticket's work). */\n const drawn = screenSections(sections);\n if (drawn > MAX_SECTIONS) {\n throw new Error(\n `${at} (\"${name}\") has more than ${MAX_SECTIONS} sections on one screen (${drawn}). A page is a screen, not a report: ` +\n 'give some of them a \"tab\", and each tab is counted as a screen of its own.',\n );\n }\n /* A record has one head. */\n if (sections.filter((s) => s.kind === 'fields' && s.presentation === 'header').length > 1) {\n throw new Error(`${at} (\"${name}\") has two record headers. A record has one head: say every fact in one header({ … }).`);\n }\n /* A strip of one tab is a heading with a border round it: the name says\n \"these are tabs of each other\", which takes two. Each region has its own. */\n for (const region of SECTION_REGIONS) {\n const tabs = new Set(sections.filter((s) => (s.region ?? 'main') === region && s.tab).map((s) => s.tab));\n if (tabs.size === 1) {\n throw new Error(\n `${at} (\"${name}\") names one tab, \"${[...tabs][0]}\"${region === 'aside' ? ' in its side panel' : ''}. A tab strip needs two tabs or more: name another, or leave \"tab\" out.`,\n );\n }\n }\n return sections;\n}\n\n/**\n * A column count, refused rather than clamped.\n *\n * Clamping `\"columns\": 7` to 3 would render a layout nobody asked for and say\n * nothing about it; the file is the thing being edited, so the file is where\n * the mistake belongs.\n */\nfunction parseFieldColumns(value: unknown, at: string): FieldColumns | undefined {\n if (value === undefined || value === null) return undefined;\n const n = Number(value);\n if (!FIELD_COLUMNS.includes(n as FieldColumns)) {\n throw new Error(`${at} has \"columns\" of ${JSON.stringify(value)}. Use 1, 2 or 3.`);\n }\n return n as FieldColumns;\n}\n\n/** A boolean the file must spell as one — `\"true\"` is a truthy string. */\nfunction parseFlag(raw: unknown, at: string, key: string): boolean | undefined {\n if (raw === undefined) return undefined;\n if (typeof raw !== 'boolean') {\n throw new Error(`${at} has \"${key}\": ${JSON.stringify(raw)}. It must be true or false, without quotes.`);\n }\n return raw || undefined;\n}\n\n/* The spellings a section may not use any more, each mapped to the word\n that replaced it. Refused by name, in the parser's usual voice, so a page\n written against the pre-rename format fails with its fix rather than\n half-working. The renames (docs/apps.md, \"The respelling\"): `whose`\n says the direction `path` hid, `about` moves the subject the\n way `of` only implied, `show` and `sort` are the query\n language's own words. */\nconst RENAMED: Record<string, string> = {\n tint: '\"tone\", as a display rule says it: { kind: { internal: \\'warn\\' } } on the timeline, or one tone on a stream',\n order: \"\\\"newest\\\": 'first' (the default) or 'last', the way a conversation reads\",\n edit: '\"editable\", as a list view says it',\n path: '\"whose\" — the column on the section\\'s table that names this record, e.g. \"customer_id\" or \"invoice_id.customer_id\"',\n of: '\"about\" — the record this section is about, named by a reference column of the base, e.g. \"site_id\"',\n paths: '\"show\" — the fields to show, e.g. [\"name\", \"account_id.name\"]',\n orderBy: '\"sort\" — written like the query bar, e.g. \"due_on desc, name\"',\n};\n\nfunction refuseRenamed(s: Record<string, unknown>, at: string, keys: string[]): void {\n for (const key of keys) {\n if (s[key] !== undefined) throw new Error(`${at} has \"${key}\", which is now ${RENAMED[key]}.`);\n }\n}\n\n/* The forms of a fields section that are gone, and what each is now, in one place: they were refused one at a time,\n each in its own words, so an upgrade met them one load after another with no sight of the rest. Every refusal of\n one of them ends with this whole table. */\nconst RETIRED_FIELDS_FORMS: ReadonlyArray<readonly [string, string]> = [\n [\"fields(…, { presentation: 'card' })\", \"card(title, 'dealer_id', ['rating', 'city']): a section of its own\"],\n [\"fields(…, { presentation: 'headline' })\", \"headline('price', { was, subfigure, note, links }): a section of its own\"],\n [\"fields(…, { presentation: 'accordion' })\", \"gone: a group of fields is always open; use 'summary', 'facts', 'table' or 'properties'\"],\n ['fields(…, { collapsed })', 'gone with the accordion: leave it out'],\n ['fields(…, { actions })', 'headline(figure, { links })'],\n ['fields(…, { subfigure | was | note })', 'headline(figure, { subfigure | was | note })'],\n];\n/** The mapping as lines for a refusal's end, without a closing full stop. */\nconst retiredFieldsForms = (): string =>\n `Fields forms that are gone, and what each is now:\\n${RETIRED_FIELDS_FORMS.map(([was, now]) => ` ${was} → ${now}`).join('\\n')}`;\n\ntype FieldPresentation = 'summary' | 'headline' | 'facts' | 'table' | 'properties' | 'card';\n/* What `fields` itself says; a headline and a card are sections of their own, and read into fields on the way. */\nconst FIELD_PRESENTATIONS: readonly FieldPresentation[] = ['summary', 'facts', 'table', 'properties', 'headline', 'card'];\n/* A field name or a label path, as `show` takes them: \"price\", \"dealer_id.name\". */\nconst FIELD_PATH = /^[A-Za-z_][A-Za-z0-9_]*(\\.[A-Za-z_][A-Za-z0-9_]*)*$/;\n\n/**\n * `measure` is the dashboard's word — \"count\", \"sum amount\" — and the one a\n * page metric or chart says, read into the planner's fn and column. Absent\n * is a count.\n */\nfunction parseMeasure(raw: unknown, at: string): { fn: PageMetric['fn']; column?: string } {\n if (raw === undefined) return { fn: 'count' };\n const m = /^\\s*(count|sum|avg|min|max)(?:\\s+(?:distinct\\s+)?([A-Za-z_][\\w]*))?(?:\\s+as\\s+\\w+)?\\s*$/i.exec(String(raw));\n if (!m) throw new Error(`${at} has a \"measure\" of \"${String(raw)}\" — write it like \"count\", \"sum amount\" or \"avg total as mean\".`);\n const fn = m[1].toLowerCase() as PageMetric['fn'];\n if (fn !== 'count' && !m[2])\n throw new Error(`${at} has a \"measure\" of \"${String(raw).trim()}\" with no column to ${fn}: write it like \"${fn} amount\".`);\n return { fn, ...(m[2] === undefined ? {} : { column: m[2].trim() }) };\n}\n\nfunction parseSection(entry: unknown, at: string, config: boolean): PageSection {\n if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {\n throw new Error(`${at} is not an object.`);\n }\n let s = entry as Record<string, unknown>;\n let kind = s.kind === undefined ? 'fields' : String(s.kind);\n /* What a tablewalk.json page may say that an App's page may not: the options only the Workbench's page builder\n sets, which an App leaves out as it leaves out a section (a hidden one is simply not written). */\n if (!config) {\n for (const key of ['hidden', 'offsetLeft', 'names', 'sizing'] as const) {\n if (s[key] !== undefined)\n throw new Error(\n `${at} has \"${key}\", which only a config file page takes. ${\n key === 'hidden'\n ? 'An App leaves a section out instead of hiding it'\n : key === 'sizing'\n ? /* Nothing on a page's related list keeps its columns from crowding; said so, rather than a replacement invented. */\n 'An App page does not use it: a related table fits its pane. For a wide one, name fewer columns in \"show\", or give the table a list view of its own, where `sizing: \\'scroll\\'` is taken, and link to it'\n : 'An App page does not use it'\n }.`,\n );\n }\n }\n /* The record's head and the current card are kinds of their own as they\n are written, so a refusal says \"header\" and \"current\" as the author\n did; the page draws them as the fields and the list they are. */\n if (kind === 'header') {\n /* `factLabels` is the page model's reading of the facts (pageModel.ts),\n and its type is published, so an author finds it there and writes it.\n A fact's word has one spelling, in `facts` itself. */\n if (Object.hasOwn(s, 'factLabels'))\n throw new Error(\n `${at} has no \"factLabels\": a fact's word is said in \"facts\" itself, as { column: 'steps', after: 'step' }. It takes: ${PAGE_SECTION_KEYS.header.join(', ')}.`,\n );\n allowKeys(s, PAGE_SECTION_KEYS.header, at);\n return parseHeader(s, at);\n }\n const current = kind === 'current';\n if (current) {\n allowKeys(s, PAGE_SECTION_KEYS.current, at);\n s = { ...s, kind: 'list', presentation: 'card' };\n kind = 'list';\n } else if (kind === 'list' && s.presentation === 'card') {\n throw new Error(\n `${at}: a related list's presentation is \"gallery\" or a cards object; the one current row as a card is its own section, current(title, from, whose, { query, sort, show }).`,\n );\n }\n /* A card and a headline are written as the sections they are and drawn as the fields they show. */\n const written = kind;\n if (written === 'card' || written === 'headline') {\n allowKeys(\n s,\n PAGE_SECTION_KEYS[written],\n at,\n written === 'headline' ? { actions: 'is now \"links\": the calls to action beside the figure' } : {},\n );\n s = { ...s, kind: 'fields', presentation: written };\n kind = 'fields';\n }\n if (s.sizing !== undefined && kind !== 'list') throw new Error(`${at}: sizing belongs on a related list.`);\n if (s.region !== undefined && !SECTION_REGIONS.includes(s.region as SectionRegion)) {\n throw new Error(`${at} has an invalid \"region\". Use \"main\" or \"aside\".`);\n }\n const tab = s.tab === undefined ? undefined : parseCaption(s.tab, `${at}.tab`);\n if (tab && tab.length > MAX_TAB_NAME) {\n throw new Error(`${at} has a \"tab\" that is not a name of at most ${MAX_TAB_NAME} characters, like \"Details\".`);\n }\n /* Where a section is drawn: its region, and the tab of that region's strip. The sections that are about the\n record's surroundings rather than the record — a card of another record, the one current row, the change\n history — stand beside it unless the author puts them in the main column. */\n const region =\n (s.region as SectionRegion | undefined) ?? (written === 'card' || current || kind === 'history' ? ('aside' as const) : undefined);\n const placement = {\n ...(region === undefined ? {} : { region }),\n ...(tab === undefined ? {} : { tab }),\n };\n if (s.create !== undefined && kind !== 'list' && kind !== 'timeline')\n throw new Error(`${at}: create belongs on a related list or a timeline.`);\n if (s.presentation !== undefined && kind !== 'fields' && kind !== 'list')\n throw new Error(`${at}: presentation belongs on fields (summary, facts, table, properties) or a related list (gallery, cards).`);\n const width = parseWidth(s.width, at);\n const offsetLeft = parseOffset(s.offsetLeft, at, 'offsetLeft');\n const offsetRight = parseOffset(s.offsetRight, at, 'offsetRight');\n const title = s.title === undefined ? undefined : isMessageRef(s.title) ? parseCaption(s.title, `${at}.title`) : String(s.title);\n const hidden = parseFlag(s.hidden, at, 'hidden');\n if (s.when !== undefined && (typeof s.when !== 'string' || !s.when.trim())) {\n throw new Error(`${at} has a \"when\" that is not a condition like \"status = converted\".`);\n }\n const when = s.when === undefined ? undefined : (s.when as string).trim();\n\n if (kind === 'fields') {\n refuseRenamed(s, at, ['paths', 'edit']);\n if (s.presentation === 'header')\n throw new Error(`${at}: the record's head is a section of its own, header({ status, facts, primary }), not a fields presentation.`);\n if (written === 'fields' && (s.presentation === 'card' || s.presentation === 'headline'))\n throw new Error(\n `${at}: a ${s.presentation} is its own section, ${\n s.presentation === 'card' ? \"card(title, 'dealer_id', ['rating', 'city'])\" : \"headline('price', { was, subfigure, note, links })\"\n }, not a fields presentation. ${retiredFieldsForms()}.`,\n );\n const authored =\n written === 'card' || written === 'headline' ? Object.fromEntries(Object.entries(s).filter(([k]) => k !== 'presentation')) : s;\n allowKeys(authored, PAGE_SECTION_KEYS[written === 'card' || written === 'headline' ? written : 'fields'], at, {\n collapsed: `is gone with the accordion: a group of fields is always open. ${retiredFieldsForms()}`,\n ...(written === 'fields'\n ? {\n actions: `belongs on a headline(figure, { links }). ${retiredFieldsForms()}`,\n subfigure: `belongs on a headline(figure, { subfigure }). ${retiredFieldsForms()}`,\n was: `belongs on a headline(figure, { was }). ${retiredFieldsForms()}`,\n note: `belongs on a headline(figure, { note }). ${retiredFieldsForms()}`,\n }\n : {}),\n });\n if (s.show !== undefined && !Array.isArray(s.show)) {\n throw new Error(`${at} has a \"show\" that is not an array of field names.`);\n }\n if (s.presentation !== undefined && !FIELD_PRESENTATIONS.includes(s.presentation as FieldPresentation)) {\n throw new Error(\n `${at}: a fields presentation is \"summary\", \"facts\", \"table\" or \"properties\"${\n s.presentation === 'accordion' ? `; the accordion is gone: a group of fields is always open. ${retiredFieldsForms()}` : ''\n }.`,\n );\n }\n if (written === 'headline' && (!Array.isArray(s.show) || !s.show.length)) {\n throw new Error(`${at}: a headline shows its figure and its facts; name them in \"show\", the figure first.`);\n }\n if (s.presentation === 'facts' && (!Array.isArray(s.show) || !s.show.length)) {\n throw new Error(`${at}: a facts line says the values it names; name them in \"show\".`);\n }\n if (written === 'card' && (!Array.isArray(s.show) || !s.show.length)) {\n throw new Error(`${at}: a card is titled by its first field and says the rest as facts; name them in \"show\", the title first.`);\n }\n if (s.editable !== undefined && s.editable !== true)\n throw new Error(\n `${at} has \"editable\": ${JSON.stringify(s.editable)}. It is true, to edit the properties in place; leave it out otherwise.`,\n );\n if (s.editable === true && s.presentation !== undefined && !['summary', 'properties'].includes(s.presentation as string)) {\n throw new Error(\n `${at}: \"editable\" belongs on the fields a reader edits — a summary or a property rail; a ${String(s.presentation)} says values, it does not hold them.`,\n );\n }\n if (s.was !== undefined && (typeof s.was !== 'string' || !FIELD_PATH.test(s.was.trim()))) {\n throw new Error(`${at} has a \"was\" that is not a field name, like \"was_price\": the figure's earlier value.`);\n }\n if (s.subfigure !== undefined && (typeof s.subfigure !== 'string' || !FIELD_PATH.test(s.subfigure.trim()))) {\n throw new Error(`${at} has a \"subfigure\" that is not a field name or a label path like \"account_id.name\".`);\n }\n if (s.note !== undefined && (typeof s.note !== 'string' || !s.note.trim() || s.note.trim().length > 300)) {\n throw new Error(`${at} has a \"note\" that is not text of at most 300 characters.`);\n }\n /* A reference says the record it names by default in an App, so the\n one thing worth writing is the opt-out. */\n if (s.names !== undefined && s.names !== false) {\n throw new Error(\n `${at} has \"names\": ${JSON.stringify(s.names)}. A reference shows the record it names by default; say false to keep the raw id, or leave it out.`,\n );\n }\n return {\n kind: 'fields',\n ...placement,\n title,\n width,\n offsetLeft,\n offsetRight,\n hidden,\n ...(when ? { when } : {}),\n paths: (s.show as string[] | undefined)?.map(String),\n columns: parseFieldColumns(s.columns, at),\n names: s.names === false ? false : undefined,\n ...(s.presentation === undefined ? {} : { presentation: s.presentation as FieldPresentation }),\n ...(s.editable === true ? { edit: true as const } : {}),\n ...(s.links === undefined ? {} : { actions: parseHeadlineActions(s.links, at) }),\n ...(s.subfigure === undefined ? {} : { subfigure: (s.subfigure as string).trim() }),\n ...(s.was === undefined ? {} : { was: (s.was as string).trim() }),\n ...(s.note === undefined ? {} : { note: (s.note as string).trim() }),\n };\n }\n\n if (kind === 'timeline') {\n refuseRenamed(s, at, ['tint', 'order']);\n allowKeys(s, PAGE_SECTION_KEYS.timeline, at);\n if (!Array.isArray(s.streams) || !s.streams.length) {\n throw new Error(`${at} is a timeline and needs \"streams\" — the dated child rows it merges.`);\n }\n if (s.streams.length > MAX_STREAMS) {\n throw new Error(`${at} merges more than ${MAX_STREAMS} streams.`);\n }\n const streams = (s.streams as unknown[]).map((raw, i) => {\n const here = `${at}, stream ${i + 1}`;\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) throw new Error(`${here} is not an object.`);\n const t = raw as Record<string, unknown>;\n refuseRenamed(t, here, ['path', 'tint']);\n allowKeys(t, STREAM_KEYS, here);\n const from = typeof t.from === 'string' ? t.from.trim() : '';\n if (!from) throw new Error(`${here} has no \"from\" table.`);\n const path = typeof t.whose === 'string' ? t.whose.trim() : '';\n if (!path) throw new Error(`${here} has no \"whose\" — the column that leads back to this record.`);\n const atCol = typeof t.at === 'string' ? t.at.trim() : '';\n if (!atCol) throw new Error(`${here} has no \"at\" — the date column the stream is ordered by.`);\n const show = typeof t.show === 'string' ? t.show.trim() : '';\n if (!show) throw new Error(`${here} has no \"show\" — the line each row becomes.`);\n /* `query` is the one spelling; the internal stream keeps its `filter`.\n `author` says who wrote a row, where `by` groups one everywhere else;\n the internal stream keeps its `by`. */\n const filterRaw = t.query;\n /* `about` as a list's: the stream's rows lead to the row a reference of the record names (a deal's company),\n not to the record, so a timeline reaches its parent's other children. */\n if (t.about !== undefined && (typeof t.about !== 'string' || !t.about.trim() || t.about.includes('.')))\n throw new Error(`${here} has an \"about\" that is not a reference column of the page's table, e.g. \"company_id\".`);\n /* The stream's own far side, where its rows name the other party in their own words (`person_id`, not `author_id`). */\n if (t.side !== undefined && (typeof t.side !== 'string' || !t.side.trim()))\n throw new Error(`${here} has a \"side\" that is not a condition like \"person_id = record.caller_id\".`);\n /* The stream's own dress, where every row of it is one kind (time logged, a system event): no column to name. */\n if (t.tone !== undefined && !(TIMELINE_TONES as readonly unknown[]).includes(t.tone))\n throw new Error(`${here} has a \"tone\" that is not one of ${TIMELINE_TONES.join(', ')}: every row of this stream wears it.`);\n if (t.compact !== undefined && t.compact !== true)\n throw new Error(`${here} has a \"compact\" that is not true: every row of this stream is one line, or leave it out.`);\n return {\n ...(typeof t.about === 'string' ? { of: t.about.trim() } : {}),\n from,\n path,\n at: atCol,\n show,\n detail: t.detail === undefined ? undefined : String(t.detail).trim() || undefined,\n by: t.author === undefined ? undefined : String(t.author).trim() || undefined,\n filter: filterRaw === undefined ? undefined : String(filterRaw).trim() || undefined,\n ...(typeof t.side === 'string' ? { side: t.side.trim() } : {}),\n ...(t.tone === undefined ? {} : { tone: t.tone as TimelineTone }),\n ...(t.compact === true ? { compact: true as const } : {}),\n };\n });\n const limit = s.limit === undefined ? undefined : Number(s.limit);\n if (limit !== undefined && (!Number.isInteger(limit) || limit < 1)) {\n throw new Error(`${at} has a \"limit\" that is not a positive whole number.`);\n }\n if (s.newest !== undefined && s.newest !== 'first' && s.newest !== 'last') {\n throw new Error(`${at} has a \"newest\" that is neither 'first' (the default) nor 'last' (a conversation, read down to the latest).`);\n }\n const composer = s.composer === undefined ? undefined : parseTimelineComposer(s.composer, at);\n const tone = s.tone === undefined ? undefined : parseTimelineTone(s.tone, at);\n const compact = s.compact === undefined ? undefined : parseTimelineCompact(s.compact, at);\n /* The other party's rows: a condition in the query language over each stream's rows, where `record.<column>` is\n the page's record. Judged per stream against the catalog with the rest of the section (page.ts timelineSides). */\n if (s.side !== undefined && (typeof s.side !== 'string' || !s.side.trim())) {\n throw new Error(`${at} has a \"side\" that is not a condition like \"author_id = record.caller_id\".`);\n }\n const side = typeof s.side === 'string' ? s.side.trim() : undefined;\n if (composer && s.create !== undefined) {\n throw new Error(\n `${at} has both \"composer\" and \"create\": a conversation's composer is the commands' (reply, note), and \"create\" is the form's. Keep one.`,\n );\n }\n return {\n kind: 'timeline',\n ...placement,\n title,\n streams,\n limit,\n width,\n offsetLeft,\n offsetRight,\n hidden,\n ...(s.newest === 'last' ? { newest: 'last' as const } : {}),\n ...(composer ? { composer } : {}),\n ...(tone ? { tone } : {}),\n ...(compact ? { compact } : {}),\n ...(side ? { side } : {}),\n ...(s.create === undefined ? {} : { create: parseRelatedCreate(s.create, at) }),\n ...(when ? { when } : {}),\n };\n }\n\n if (kind === 'custom') {\n allowKeys(s, PAGE_SECTION_KEYS.custom, at, CUSTOM_RENAMED);\n /* The author's own component, placed like any section: its reads are\n declared, so the planner checks and runs them (appComponents.ts). */\n const component = parseComponentDecl(s.component, `${at}, \"component\"`, 'section');\n const parsed = parsePageReads(s.rows, at);\n const reads =\n parsed &&\n Object.fromEntries(\n Object.entries(parsed).map(([name, read]) => [\n name,\n read.sort === undefined ? read : { ...read, orderBy: parseSort(read.sort, `${at}, rows \"${name}\"`) },\n ]),\n );\n const totals = parsePageTotals(s.figures, at);\n return {\n kind: 'custom',\n ...placement,\n title,\n width,\n offsetLeft,\n offsetRight,\n hidden,\n ...(when ? { when } : {}),\n component,\n ...(s.record === undefined ? {} : { record: parseRecordFields(s.record, at) }),\n ...(reads ? { reads } : {}),\n ...(totals ? { aggregates: totals } : {}),\n props: componentPropValues(component, s.props, at),\n };\n }\n\n if (kind === 'history') {\n allowKeys(s, PAGE_SECTION_KEYS.history, at);\n const limit = s.limit === undefined ? undefined : Number(s.limit);\n if (limit !== undefined && (!Number.isInteger(limit) || limit < 1 || limit > MAX_HISTORY_LIMIT)) {\n throw new Error(`${at} has a \"limit\" that is not a whole number from 1 to ${MAX_HISTORY_LIMIT}.`);\n }\n return {\n kind: 'history',\n ...placement,\n title,\n width,\n offsetLeft,\n offsetRight,\n hidden,\n ...(limit === undefined ? {} : { limit }),\n ...(when ? { when } : {}),\n };\n }\n\n if (kind === 'flows' || kind === 'diagram') {\n allowKeys(s, PAGE_SECTION_KEYS[kind], at);\n return {\n kind,\n ...placement,\n title,\n width,\n offsetLeft,\n offsetRight,\n hidden,\n ...(when ? { when } : {}),\n };\n }\n\n if (kind === 'note') {\n allowKeys(s, PAGE_SECTION_KEYS.note, at);\n if (typeof s.text !== 'string' || !s.text.trim()) throw new Error(`${at} is a note and has no \"text\" — the words it says.`);\n let action: AppLinkAction | undefined;\n if (s.action !== undefined) {\n action = parseAppLinkAction(s.action, `${at}, \"action\"`, true);\n }\n if (s.variant !== undefined && s.variant !== 'quiet' && s.variant !== 'callout') {\n throw new Error(\n `${at} has a \"variant\" of ${JSON.stringify(s.variant)}: a note is \"quiet\" (small print, no mark) or a \"callout\" (a ground of its own); leave it out for the plain note.`,\n );\n }\n return {\n kind: 'note',\n ...placement,\n title,\n width,\n offsetLeft,\n offsetRight,\n hidden,\n ...(when ? { when } : {}),\n ...(s.variant === undefined ? {} : { variant: s.variant as 'quiet' | 'callout' }),\n text: s.text.trim(),\n ...(s.icon === undefined ? {} : { icon: parseIcon(s.icon, at) }),\n ...(action ? { action } : {}),\n };\n }\n\n if (kind === 'view') {\n allowKeys(s, PAGE_SECTION_KEYS.view, at);\n if (typeof s.view !== 'string' || !s.view.trim()) throw new Error(`${at} embeds no \"view\" — the key of one of the App's list views.`);\n const path = typeof s.whose === 'string' ? s.whose.trim() : '';\n if (!path) {\n throw new Error(\n `${at} embeds \"${s.view.trim()}\" with no \"whose\" — on a page, an embedded list shows the rows *whose* named column ` +\n \"is this record: embed('invoices', { whose: 'customer_id' }).\",\n );\n }\n if (s.about !== undefined && (typeof s.about !== 'string' || !s.about.trim())) {\n throw new Error(`${at} has an \"about\" that is not a non-empty string.`);\n }\n if (s.limit !== undefined && (typeof s.limit !== 'number' || !Number.isInteger(s.limit) || s.limit < 1)) {\n throw new Error(`${at} has a \"limit\" that is not a positive whole number.`);\n }\n return {\n kind: 'list',\n ...placement,\n title,\n width,\n offsetLeft,\n offsetRight,\n hidden,\n ...(when ? { when } : {}),\n view: s.view.trim(),\n from: '',\n path,\n ...(s.about === undefined ? {} : { of: (s.about as string).trim() }),\n ...(s.limit === undefined ? {} : { limit: s.limit as number }),\n };\n }\n\n if (kind !== 'metric' && kind !== 'list' && kind !== 'breakdown' && kind !== 'chart') {\n throw new Error(\n `${at} has an unknown kind \"${kind}\". Use \"fields\", \"metric\", \"list\", \"breakdown\", \"chart\", \"note\", \"timeline\", \"history\", \"flows\", \"diagram\", \"custom\" or \"view\" (an embedded list view); ` +\n 'in an App, each is written with its helper: fields(), metric(), related(), breakdown(), chart(), note(), timeline(), history(), flows(), diagram(), custom(), embed().',\n );\n }\n\n refuseRenamed(s, at, kind === 'list' ? ['path', 'of', 'orderBy'] : ['path', 'of', 'orderBy', 'edit']);\n if (kind === 'list' && s.columns !== undefined) {\n throw new Error(`${at} has \"columns\", which is now \"show\" — the columns the list shows, e.g. [\"due_on\", \"status\"].`);\n }\n allowKeys(\n s,\n PAGE_SECTION_KEYS[kind],\n at,\n kind === 'list'\n ? {\n about: 'a related list no longer takes: a row that shares a column with the record is found by { same: [column] }',\n similar: \"is now the list's own match: related(title, from, { same: [column], near: { column: 0.25 } })\",\n editable: 'is gone: the lines of a record are edited in its form (a form\\'s \"lines\")',\n order: 'is gone with the editable lines; a list sorts with \"sort\"',\n }\n : {},\n );\n const from = typeof s.from === 'string' ? s.from.trim() : '';\n if (!from) throw new Error(`${at} has no \"from\" table.`);\n const similar = kind === 'list' && s.same !== undefined ? parseMatch(s, at) : undefined;\n if (kind === 'list' && s.near !== undefined && s.same === undefined)\n throw new Error(`${at} has \"near\" without \"same\": a match is the columns a row shares with the record, and the numbers it keeps near.`);\n const path = similar ? '' : typeof s.whose === 'string' ? s.whose.trim() : '';\n if (!path && !similar) {\n throw new Error(\n `${at} has no \"whose\" — the rows shown are the ${from} rows *whose* named column ` +\n 'is this record: \"customer_id\" for the customer\\'s own rows, ' +\n '\"invoice_id.customer_id\" for rows two references away; or, to list the rows that share its values, \"same\".',\n );\n }\n /* `query` is the one word for narrowing rows, on a page section as on a\n dashboard's; the internal section keeps its `filter`. */\n const filter = s.query === undefined ? undefined : String(s.query).trim() || undefined;\n /* `about` moves a figure's subject one reference away from the\n root: \"tickets at this order's site\" is { from: \"ticket\", whose:\n \"site_id\", about: \"site_id\" }. Absent means the root itself. */\n if (s.about !== undefined && (typeof s.about !== 'string' || !s.about.trim())) {\n throw new Error(`${at} has an \"about\" that is not a non-empty string.`);\n }\n const of = s.about === undefined ? undefined : (s.about as string).trim();\n\n /* A figure said as a dashboard's is (appViews.ts): an average of a 1–5 rating has no column rule to say it, and\n drew \"3.333\" with nothing after it; a win rate `avg won * 100` is a percent (G111). */\n if (s.format !== undefined && !(FIGURE_FORMATS as readonly string[]).includes(String(s.format)))\n throw new Error(`${at} has a \"format\" of \"${String(s.format)}\". Use ${FIGURE_FORMAT_WORDS}.`);\n if (s.unit !== undefined && (typeof s.unit !== 'string' || !s.unit.trim() || s.unit.length > 24))\n throw new Error(`${at} has a \"unit\" that is not a short word said after the figure, like \"days\".`);\n const figureWords = {\n ...(s.format === undefined ? {} : { format: s.format as PageMetric['format'] }),\n ...(s.unit === undefined ? {} : { unit: (s.unit as string).trim() }),\n };\n\n if (kind === 'metric') {\n const { fn, column: columnRaw } = parseMeasure(s.measure, at);\n return {\n kind: 'metric',\n ...placement,\n title,\n width,\n offsetLeft,\n offsetRight,\n hidden,\n ...(when ? { when } : {}),\n fn: fn as PageMetric['fn'],\n from,\n path,\n of,\n column: columnRaw === undefined ? undefined : String(columnRaw).trim(),\n filter,\n ...figureWords,\n };\n }\n\n if (kind === 'chart') {\n const by = typeof s.by === 'string' ? s.by.trim() : '';\n if (!by) throw new Error(`${at} has no \"by\" — what its groups are: \"status\", or a bucketed date like \"month issued_on\".`);\n if (s.shape !== undefined && !(CHART_SHAPES as readonly string[]).includes(String(s.shape))) {\n throw new Error(`${at} has a \"shape\" of \"${String(s.shape)}\". Use \"line\", \"bar\", \"donut\" or \"stacked\".`);\n }\n if (s.shape === 'stacked' && !by.includes(',')) {\n throw new Error(\n `${at} is a stacked chart and needs a two-key \"by\" — \"month issued_on, status\": a bar for each month, divided by status.`,\n );\n }\n if (s.limit !== undefined && (typeof s.limit !== 'number' || !Number.isInteger(s.limit) || s.limit < 1)) {\n throw new Error(`${at} has a \"limit\" that is not a positive whole number.`);\n }\n if (s.height !== undefined && (typeof s.height !== 'number' || !Number.isFinite(s.height) || s.height < 120)) {\n throw new Error(`${at} has a \"height\" that is not a number of pixels (120 or more).`);\n }\n if (s.target !== undefined && (typeof s.target !== 'number' || !Number.isFinite(s.target))) {\n throw new Error(`${at} has a \"target\" that is not a number.`);\n }\n if (s.target !== undefined && s.shape === 'donut')\n throw new Error(`${at} is a donut, which has no scale to draw a \"target\" on. Use a line or stacked bars.`);\n let sort: string | undefined;\n if (s.sort !== undefined) {\n if (s.shape !== 'bar' && s.shape !== 'donut')\n throw new Error(`${at} has a \"sort\", which orders the categories of a bar chart or a donut; a line runs along its key.`);\n if (s.sort === 'value' || s.sort === 'category') sort = s.sort;\n else {\n const parsed = parseSortText(s.sort, at, 'starts_on');\n if (parsed!.length !== 1 || parsed![0].column.includes('.'))\n throw new Error(\n `${at} has a \"sort\" of more than one column or a path; categories are ordered by 'value', 'category' or one column of the rows: \"starts_on desc\".`,\n );\n sort = parsed![0].direction === 'desc' ? `${parsed![0].column} desc` : parsed![0].column;\n }\n }\n const { fn, column } = parseMeasure(s.measure, at);\n return {\n kind: 'chart',\n ...placement,\n title,\n width,\n offsetLeft,\n offsetRight,\n hidden,\n ...(when ? { when } : {}),\n from,\n path,\n of,\n by,\n fn,\n ...(column === undefined ? {} : { column }),\n ...(s.shape === undefined ? {} : { shape: s.shape as PageChart['shape'] }),\n ...(sort ? { sort } : {}),\n filter,\n ...(s.limit === undefined ? {} : { limit: s.limit as number }),\n ...(s.height === undefined ? {} : { height: s.height as number }),\n ...(s.target === undefined ? {} : { target: s.target as number }),\n ...figureWords,\n };\n }\n\n if (kind === 'breakdown') {\n const by = typeof s.by === 'string' ? s.by.trim() : '';\n if (!by) {\n throw new Error(`${at} has no \"by\" — the column its groups are counted under.`);\n }\n if (s.limit !== undefined && (typeof s.limit !== 'number' || !Number.isInteger(s.limit) || s.limit < 1)) {\n throw new Error(`${at} has a \"limit\" that is not a positive whole number.`);\n }\n const measure = parseMeasure(s.measure, at);\n return {\n kind: 'breakdown',\n // A count is what a breakdown says when it says nothing.\n ...(measure.fn === 'count' && measure.column === undefined\n ? {}\n : { fn: measure.fn, ...(measure.column === undefined ? {} : { column: measure.column }) }),\n ...placement,\n title,\n width,\n offsetLeft,\n offsetRight,\n hidden,\n ...(when ? { when } : {}),\n from,\n path,\n of,\n by,\n filter,\n limit: s.limit as number | undefined,\n ...figureWords,\n };\n }\n\n if (s.limit !== undefined && (typeof s.limit !== 'number' || !Number.isInteger(s.limit) || s.limit < 1)) {\n throw new Error(`${at} has a \"limit\" that is not a positive whole number.`);\n }\n if (s.show !== undefined && !Array.isArray(s.show)) {\n throw new Error(`${at} has a \"show\" that is not an array of column names.`);\n }\n const shown = s.show === undefined ? undefined : parseShowEntries(s.show as unknown[], at);\n /* A gallery draws a related list of image files; whether the rows are\n files is the App's to say (`file` on the resource), judged where the\n page meets the App. */\n /* Or its rows as cards, in the list view's own cards grammar: the same\n keys, judged against the catalog where the page meets the App. */\n let cards: AppCardsList | undefined;\n /* The one current row as a card — the active lease, the latest visit —\n chosen by the list's query and sort: one row, so nothing to page, sum\n or add under it. */\n if (current && (s.empty === false || s.empty === 'hide' || s.empty === 'collapse'))\n throw new Error(`${at} is a current() card; say what no row means with \"empty\" guidance rather than leaving it out.`);\n if (s.presentation !== undefined && s.presentation !== 'gallery' && !current) {\n const raw = s.presentation;\n if (!raw || typeof raw !== 'object' || Array.isArray(raw) || (raw as Record<string, unknown>).kind !== 'cards') {\n throw new Error(`${at}: a related list's presentation is \"gallery\" or a cards object { kind: 'cards', primary, … }.`);\n }\n const c = raw as Record<string, unknown>;\n allowKeys(\n c,\n ['kind', 'primary', 'description', 'value', 'badge', 'icon', 'properties', 'facts', 'footer', 'image', 'media'],\n `${at}, presentation (cards)`,\n { lines: 'is now \"properties\": the captioned values under the title (\"facts\" stays the dotted line)' },\n );\n if (typeof c.primary !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*(\\.[A-Za-z_][A-Za-z0-9_]*)?$/.test(c.primary.trim()))\n throw new Error(`${at}, presentation (cards): \"primary\" must be a local column or a one-hop path like \"vehicle_id.title\".`);\n if (\n c.properties !== undefined &&\n (!Array.isArray(c.properties) ||\n c.properties.length > CARDS_MAX_LINES ||\n c.properties.some((line) => typeof line !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*(\\.[A-Za-z_][A-Za-z0-9_]*)?$/.test(line.trim())))\n ) {\n throw new Error(`${at}, presentation (cards): \"properties\" must be at most ${CARDS_MAX_LINES} local columns or one-hop label paths.`);\n }\n cards = parseCards(\n c,\n `${at}, presentation (cards)`,\n c.primary.trim(),\n ((c.properties as string[] | undefined) ?? []).map((line) => line.trim()),\n );\n }\n /* `empty` is the guidance a list with no rows shows, or what to do with the list instead: 'hide' leaves it out\n until it has a row (a manager's direct reports, on a page most people's are none), 'collapse' says it in one\n quiet line with the other empty lists beside it. Where the reader may add a row, 'hide' collapses rather than\n hides: a list that offers a door is never taken away (PageSections.tsx). */\n if (s.empty === true)\n throw new Error(\n `${at} has \"empty: true\". \"empty\" is guidance ({ title, description, action }), 'hide' to leave the list out while it has no rows, or 'collapse' to say so in one line.`,\n );\n if (s.empty === false) throw new Error(`${at} has \"empty: false\", which is now \"empty: 'hide'\".`);\n const emptyMode = s.empty === 'hide' || s.empty === 'collapse' ? s.empty : undefined;\n /* A footer of sums, the list view's `totals`: over every row that points\n at the record, not the page, so it is one aggregate over the rows the\n path reaches and never through a filter. */\n let totals: string[] | undefined;\n if (s.totals !== undefined) {\n if (\n !Array.isArray(s.totals) ||\n !s.totals.length ||\n s.totals.some((c) => typeof c !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(c))\n ) {\n throw new Error(`${at} has \"totals\" that are not a list of the rows' numeric columns, rollups or computed numbers.`);\n }\n if (new Set(s.totals).size !== s.totals.length) throw new Error(`${at} has \"totals\" that name one column twice.`);\n if (s.presentation !== undefined)\n throw new Error(\n `${at} has \"totals\" and a ${s.presentation === 'gallery' ? 'gallery' : 'cards'} presentation; a footer of totals belongs to a table.`,\n );\n const narrowed = ['query', 'near'].filter((key) => s[key] !== undefined && s[key] !== false);\n if (narrowed.length) {\n throw new Error(\n `${at} has \"totals\" and \"${narrowed[0]}\". Totals sum every row that points at the record and cannot be narrowed; drop one, or add a total() metric beside the list.`,\n );\n }\n totals = s.totals as string[];\n }\n const group = parseGroup(s, at, { similar: Boolean(similar), presentation: s.presentation, current });\n /* The child's own verbs, from a ⋯ at the end of each row, as its own page offers them: a table's, never a card's or a photo's. */\n if (s.rowActions !== undefined && s.rowActions !== true && s.rowActions !== false)\n throw new Error(`${at}: \"rowActions\" must be true (a ⋯ menu on each row with the row's own actions) or false.`);\n if (s.rowActions === true && s.presentation !== undefined)\n throw new Error(`${at} has \"rowActions\" and a presentation; a row menu belongs to a table.`);\n /* The record's own verbs that add to this list — \"Add to the committee\" on a deal — drawn in the list's head\n rather than the record's, by the ids its resource's actions have (checked against them where the App is). */\n let listActions: string[] | undefined;\n if (s.actions !== undefined) {\n if (\n !Array.isArray(s.actions) ||\n !s.actions.length ||\n s.actions.length > 4 ||\n s.actions.some((id) => typeof id !== 'string' || !id.trim())\n )\n throw new Error(`${at}: \"actions\" is one to four of the record's own action ids, drawn in the list's head: ['addBuyer'].`);\n listActions = (s.actions as string[]).map((id) => id.trim());\n if (new Set(listActions).size !== listActions.length) throw new Error(`${at} has \"actions\" naming one action twice.`);\n }\n return {\n kind: 'list',\n ...placement,\n ...(s.sizing === undefined ? {} : { sizing: parseTableSizing(s.sizing, at) }),\n ...(s.presentation === 'gallery' || s.presentation === 'card'\n ? { presentation: s.presentation }\n : cards\n ? { presentation: cards }\n : {}),\n ...(similar ? { similar } : {}),\n title,\n width,\n offsetLeft,\n offsetRight,\n hidden,\n ...(when ? { when } : {}),\n from,\n path,\n columns: shown?.columns,\n ...(shown?.labels ? { labels: shown.labels } : {}),\n filter,\n limit: s.presentation === 'card' ? 1 : (s.limit as number | undefined),\n orderBy: parseSort(s.sort, at),\n ...(s.create === undefined ? {} : s.create === false ? { noCreate: true as const } : { create: parseRelatedCreate(s.create, at) }),\n ...(s.empty === undefined || emptyMode ? {} : { empty: parseListEmpty(s.empty, at) }),\n ...(emptyMode ? { emptyMode } : {}),\n ...(group ? { group } : {}),\n ...(totals ? { totals } : {}),\n ...(s.rowActions === true ? { rowActions: true as const } : {}),\n ...(listActions ? { listActions } : {}),\n };\n}\n\n/**\n * A related list's `group`: one header for each record its rows share through `by`, the rows to a group under it.\n * One level only. `limit` beside it is the rows to a group, so it is held to what a group opens to.\n */\nfunction parseGroup(\n s: Record<string, unknown>,\n at: string,\n beside: { similar: boolean; presentation: unknown; current: boolean },\n): PageGroup | undefined {\n if (s.group === undefined) return undefined;\n const raw = s.group;\n if (!raw || typeof raw !== 'object' || Array.isArray(raw))\n throw new Error(\n `${at} has a \"group\" that is not an object: group: { by: 'estimate_id', show: ['vehicle_id', 'total'], sort: 'created_on desc', limit: 5 }.`,\n );\n if (beside.current) throw new Error(`${at} is a current() card, which is one row and has nothing to group.`);\n if (beside.similar)\n throw new Error(`${at} has \"group\" and \"same\": a matched list is not the record's children, so it has no chain worth grouping.`);\n if (beside.presentation !== undefined)\n throw new Error(\n `${at} has \"group\" and a ${beside.presentation === 'gallery' ? 'gallery' : 'cards'} presentation; groups are drawn as tables.`,\n );\n const g = raw as Record<string, unknown>;\n allowKeys(g, ['by', 'show', 'sort', 'limit', 'empty', 'sum'], `${at}, group`);\n if (typeof g.by !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*(\\.[A-Za-z_][A-Za-z0-9_]*)*$/.test(g.by.trim()))\n throw new Error(`${at}, group has no \"by\": the column or reference the rows are grouped under, like \"estimate_id\".`);\n if (g.show !== undefined) {\n if (\n !Array.isArray(g.show) ||\n !g.show.length ||\n g.show.length > 4 ||\n g.show.some((c) => typeof c !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(c))\n )\n throw new Error(`${at}, group has a \"show\" that is not one to four columns of the record each group is: ['vehicle_id', 'total'].`);\n if (new Set(g.show).size !== g.show.length) throw new Error(`${at}, group has a \"show\" that names one column twice.`);\n }\n let sort: PageGroup['sort'];\n if (g.sort !== undefined) {\n const parsed = parseSortText(g.sort, `${at}, group`, 'created_on desc')!;\n if (parsed.length !== 1 || parsed[0].column.includes('.'))\n throw new Error(\n `${at}, group has a \"sort\" of more than one column or a path; groups are ordered by one column of their record: \"created_on desc\".`,\n );\n sort = { column: parsed[0].column, direction: parsed[0].direction };\n }\n if (g.limit !== undefined && (typeof g.limit !== 'number' || !Number.isInteger(g.limit) || g.limit < 1 || g.limit > GROUPS_MOST))\n throw new Error(`${at}, group has a \"limit\" that is not a whole number of groups from 1 to ${GROUPS_MOST}.`);\n // A msg() is said by the loader before a page is read; one that reaches here has no catalog to say it.\n if (isMessageRef(g.empty)) parseCaption(g.empty, `${at}.group.empty`);\n if (g.empty !== undefined && (typeof g.empty !== 'string' || !g.empty.trim() || g.empty.length > 60))\n throw new Error(`${at}, group has an \"empty\" that is not a short name for the rows with no value: 'Backlog'.`);\n let sum: string[] | undefined;\n if (g.sum !== undefined) {\n const named = typeof g.sum === 'string' ? [g.sum] : g.sum;\n if (\n !Array.isArray(named) ||\n named.length < 1 ||\n named.length > 2 ||\n named.some((c) => typeof c !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(c))\n )\n throw new Error(`${at}, group has a \"sum\" that is not one or two numeric columns of the rows: 'points' or ['points', 'hours'].`);\n if (new Set(named).size !== named.length) throw new Error(`${at}, group has a \"sum\" that names one column twice.`);\n sum = named as string[];\n }\n if (typeof s.limit === 'number' && s.limit > GROUP_ROWS_MOST)\n throw new Error(`${at} has a \"limit\" of ${s.limit} beside \"group\", where it is the rows to a group: at most ${GROUP_ROWS_MOST}.`);\n return {\n by: g.by.trim(),\n ...(g.show ? { show: g.show as string[] } : {}),\n ...(sort ? { sort } : {}),\n ...(g.limit === undefined ? {} : { limit: g.limit as number }),\n ...(g.empty === undefined ? {} : { empty: (g.empty as string).trim() }),\n ...(sum ? { sum } : {}),\n };\n}\n\n/* The record's header: the head the page already draws — its noun over the\n name, the verbs beside it — with the status chip and the facts line an\n author names. It is drawn in the head, not where it is written, so what\n would move it (a region, a tab, a width) is refused rather than ignored. */\nfunction parseHeader(s: Record<string, unknown>, at: string): PageSection {\n if (s.status !== undefined && (typeof s.status !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(s.status.trim()))) {\n throw new Error(`${at} has a \"status\" that is not one of the record's own columns, like \"stage\".`);\n }\n if (\n s.destructive !== undefined &&\n (!Array.isArray(s.destructive) || !s.destructive.length || s.destructive.some((id) => typeof id !== 'string' || !id.trim()))\n ) {\n throw new Error(`${at} has a \"destructive\" that is not a list of the record's action ids, like ['markLost'].`);\n }\n /* The one verb drawn filled: the one the record most needs next, which\n is never one that takes something away. Whether each id is one of the\n base resource's actions is the App's to judge (apps.ts), where both are known. */\n if (s.primary !== undefined && (typeof s.primary !== 'string' || !s.primary.trim())) {\n throw new Error(`${at} has a \"primary\" that is not the id of one of the record's actions, like 'renew'.`);\n }\n if (\n s.actions !== undefined &&\n (!Array.isArray(s.actions) ||\n !s.actions.length ||\n s.actions.some((id) => typeof id !== 'string' || !id.trim()) ||\n new Set(s.actions.map((id) => String(id).trim())).size !== s.actions.length)\n ) {\n throw new Error(`${at} has \"actions\" that are not a list of the record's action ids, each once, like ['resolve', 'reassign'].`);\n }\n /* The filled verb is one the head draws. */\n if (\n Array.isArray(s.actions) &&\n typeof s.primary === 'string' &&\n !s.actions.some((id) => String(id).trim() === (s.primary as string).trim())\n ) {\n throw new Error(`${at} has \"primary\" \"${s.primary}\", which is not one of its \"actions\": the head cannot fill a verb it does not draw.`);\n }\n if (\n typeof s.primary === 'string' &&\n Array.isArray(s.destructive) &&\n s.destructive.some((id) => typeof id === 'string' && id.trim() === (s.primary as string).trim())\n ) {\n throw new Error(\n `${at} names \"${s.primary}\" both \"primary\" and \"destructive\": the filled button is the forward verb, and a destructive one is drawn last and quiet.`,\n );\n }\n /* A header with only a `primary` or its `destructive` verbs is the head\n the page draws anyway, told which verb to fill: worth writing. One that\n says nothing is a slip. */\n if (['status', 'facts', 'primary', 'destructive', 'actions'].every((key) => s[key] === undefined)) {\n throw new Error(\n `${at}: a record header says something: a \"status\", \"facts\", its \"primary\" verb, its \"destructive\" ones or the \"actions\" it draws.`,\n );\n }\n const facts = s.facts === undefined ? undefined : parseFacts(s.facts, `${at}, header`);\n return {\n kind: 'fields',\n presentation: 'header',\n paths: facts?.facts ?? [],\n ...(facts?.factLabels ? { factLabels: facts.factLabels } : {}),\n ...(s.status === undefined ? {} : { status: (s.status as string).trim() }),\n ...(s.primary === undefined ? {} : { primary: (s.primary as string).trim() }),\n ...(s.destructive === undefined ? {} : { destructive: (s.destructive as string[]).map((id) => id.trim()) }),\n ...(s.actions === undefined ? {} : { headerActions: (s.actions as string[]).map((id) => id.trim()) }),\n };\n}\n\n/* A conversation's composer: the commands its two modes run, named as the App's actions name them. */\nfunction parseTimelineComposer(raw: unknown, at: string): TimelineComposer {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw))\n throw new Error(`${at} has a \"composer\" that is not { reply, note? }: the names of the commands a Reply and an Internal note run.`);\n const c = raw as Record<string, unknown>;\n allowKeys(c, ['reply', 'note', 'templates'], `${at}, composer`);\n for (const key of ['reply', 'note'] as const) {\n if (c[key] !== undefined && (typeof c[key] !== 'string' || !(c[key] as string).trim()))\n throw new Error(`${at}: composer.${key} is not a command's name.`);\n }\n if (typeof c.reply !== 'string') throw new Error(`${at}: a composer needs \"reply\", the command a Reply runs.`);\n const reply = c.reply.trim();\n const note = typeof c.note === 'string' ? c.note.trim() : undefined;\n if (note !== undefined && note === reply)\n throw new Error(\n `${at}: composer.reply and composer.note name the same command \"${reply}\"; a note is a different command from a reply.`,\n );\n const templates = c.templates === undefined ? undefined : parseComposerTemplates(c.templates, at);\n return { reply, ...(note === undefined ? {} : { note }), ...(templates ? { templates } : {}) };\n}\n\n/* Canned replies: the rows of a resource, each a label to pick and the text it puts in the box. */\nfunction parseComposerTemplates(raw: unknown, at: string): NonNullable<TimelineComposer['templates']> {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw))\n throw new Error(\n `${at}: composer.templates is not { from, labelColumn, text, query? }: the resource whose rows are canned replies, and their two columns.`,\n );\n const t = raw as Record<string, unknown>;\n allowKeys(t, ['from', 'labelColumn', 'text', 'query'], `${at}, composer.templates`, {\n label: 'is now \"labelColumn\": the column a reader picks a reply by',\n });\n for (const key of ['from', 'labelColumn', 'text'] as const) {\n if (typeof t[key] !== 'string' || !(t[key] as string).trim())\n throw new Error(\n `${at}: composer.templates needs \"${key}\", ${key === 'from' ? 'the resource whose rows are the canned replies' : key === 'labelColumn' ? 'the column a reader picks by' : 'the column whose words fill the box'}.`,\n );\n }\n /* Which rows are offered, beyond what the reader may read: a retired response is still readable. */\n if (t.query !== undefined && (typeof t.query !== 'string' || !t.query.trim()))\n throw new Error(`${at}: composer.templates has a \"query\" that is not a condition, like 'active = true'.`);\n return {\n from: (t.from as string).trim(),\n labelColumn: (t.labelColumn as string).trim(),\n text: (t.text as string).trim(),\n ...(typeof t.query === 'string' ? { query: t.query.trim() } : {}),\n };\n}\n\nconst TIMELINE_TONES = ['good', 'warn', 'bad', 'info', 'neutral'] as const;\n/* A tone rule says \"this column's value → a tone\": one or two columns at most, each a short list. */\nfunction parseTimelineTone(raw: unknown, at: string): Record<string, Record<string, TimelineTone>> {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw) || !Object.keys(raw).length) {\n throw new Error(`${at} has a \"tone\" that is not { column: { value: tone } }, like { kind: { internal: 'warn' } }.`);\n }\n const out: Record<string, Record<string, TimelineTone>> = {};\n for (const [column, values] of Object.entries(raw)) {\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(column))\n throw new Error(`${at}: tone names \"${column}\", which is not one of the stream's own columns.`);\n if (!values || typeof values !== 'object' || Array.isArray(values) || !Object.keys(values).length) {\n throw new Error(`${at}: tone.${column} is not { value: tone }, like { internal: 'warn' }.`);\n }\n out[column] = {};\n for (const [value, tone] of Object.entries(values)) {\n if (!(TIMELINE_TONES as readonly unknown[]).includes(tone))\n throw new Error(`${at}: tone.${column}.${value} is \"${String(tone)}\", not one of ${TIMELINE_TONES.join(', ')}.`);\n out[column][value] = tone as TimelineTone;\n }\n }\n return out;\n}\n\n/* Rows of these column values are one line: { kind: ['system'] }. */\nfunction parseTimelineCompact(raw: unknown, at: string): Record<string, string[]> {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw) || !Object.keys(raw).length) {\n throw new Error(`${at} has a \"compact\" that is not { column: [values] }, like { kind: ['system'] }.`);\n }\n const out: Record<string, string[]> = {};\n for (const [column, values] of Object.entries(raw)) {\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(column))\n throw new Error(`${at}: compact names \"${column}\", which is not one of the stream's own columns.`);\n if (!Array.isArray(values) || !values.length || values.some((value) => typeof value !== 'string' || !value)) {\n throw new Error(`${at}: compact.${column} is not a list of values, like ['system'].`);\n }\n out[column] = values.map(String);\n }\n return out;\n}\n\n/* Few enough columns that each still says why a row is on the list. */\nconst MATCH_MAX_SAME = 4;\nconst MATCH_MAX_NEAR = 2;\n\n/**\n * A list's match: the rows that share the record's own values, `same` equal\n * and `near` within a fraction. It is found by those values rather than by a\n * reference, so each word that only means something beside a reference is\n * refused here rather than ignored: there is no `whose` to walk, no parent\n * to `create` under (`empty` is kept: a match that finds nothing says so), and where the rows are the page's own table the record\n * is always left out.\n */\nfunction parseMatch(s: Record<string, unknown>, section: string): PageSimilar {\n /* Named by its title where it has one: a page may hold several related lists, and \"section 3\" alone is a count. */\n const at = typeof s.title === 'string' && s.title.trim() ? `${section} (\"${s.title.trim()}\")` : section;\n const beside: Record<string, string> = {\n whose: \"its rows share the record's values rather than point at it\",\n create:\n \"its rows are not the record's children: a row added here would need the record's values in every matched column, and a near column has no value to fill, so there is nothing to add them under (add through the list's own page)\",\n };\n for (const [key, why] of Object.entries(beside)) {\n if (s[key] !== undefined && s[key] !== false) throw new Error(`${at} has \"${key}\" beside \"same\" — ${why}.`);\n }\n const column = /^[A-Za-z_][A-Za-z0-9_]*$/;\n if (\n !Array.isArray(s.same) ||\n !s.same.length ||\n s.same.length > MATCH_MAX_SAME ||\n s.same.some((c) => typeof c !== 'string' || !column.test(c))\n ) {\n throw new Error(`${at}: \"same\" must be one to ${MATCH_MAX_SAME} columns a row shares with the record, e.g. ['body_type'].`);\n }\n const same = s.same as string[];\n if (new Set(same).size !== same.length) throw new Error(`${at}: \"same\" names a column twice.`);\n if (s.near === undefined) return { same };\n if (!s.near || typeof s.near !== 'object' || Array.isArray(s.near)) {\n throw new Error(`${at}: \"near\" must be an object of number columns and fractions, e.g. { price: 0.25 }.`);\n }\n const near = Object.entries(s.near as Record<string, unknown>);\n if (!near.length || near.length > MATCH_MAX_NEAR) {\n throw new Error(`${at}: \"near\" must name one or ${MATCH_MAX_NEAR === 2 ? 'two' : MATCH_MAX_NEAR} columns.`);\n }\n for (const [name, fraction] of near) {\n if (!column.test(name)) throw new Error(`${at}: \"near\" names \"${name}\", which is not a column name.`);\n if (same.includes(name)) throw new Error(`${at}: \"${name}\" is in both \"same\" and \"near\" — equal already is near.`);\n if (typeof fraction !== 'number' || !Number.isFinite(fraction) || fraction <= 0 || fraction > 1) {\n throw new Error(`${at}: \"near\" on \"${name}\" must be a fraction above 0 and at most 1 — 0.25 is within ±25% of the record's value.`);\n }\n }\n return { same, near: Object.fromEntries(near) as Record<string, number> };\n}\n\n/**\n * A list's `show`, in the nav's grammar: a column path, or `{ column,\n * label }` with the app's word over it. The paths stay the planner's\n * `columns`; the labels ride beside them for whoever draws the header.\n */\nfunction parseShowEntries(raw: unknown[], at: string): { columns: string[]; labels?: Record<string, string> } {\n const labels: Record<string, string> = {};\n const columns = raw.map((entry, i) => {\n if (typeof entry === 'string') return entry;\n if (entry && typeof entry === 'object' && !Array.isArray(entry)) {\n const c = entry as Record<string, unknown>;\n for (const key of Object.keys(c)) {\n if (key !== 'column' && key !== 'label') throw new Error(`${at}, \"show\" entry ${i + 1} has no \"${key}\". It takes: column, label.`);\n }\n if (typeof c.column !== 'string' || !c.column.trim()) throw new Error(`${at}, \"show\" entry ${i + 1} has no \"column\".`);\n if (c.label !== undefined && (typeof c.label !== 'string' || !c.label.trim())) {\n throw new Error(`${at}, \"show\" entry ${i + 1} has a \"label\" that is not a non-empty string.`);\n }\n if (c.label !== undefined) labels[c.column.trim()] = (c.label as string).trim();\n return c.column.trim();\n }\n throw new Error(`${at}, \"show\" entry ${i + 1} is not a column name or a { \"column\", \"label\" } object.`);\n });\n return { columns, ...(Object.keys(labels).length ? { labels } : {}) };\n}\n\n/**\n * A headline's calls to action: one or two, each opening one of the App's\n * views — a public form, a list, a dashboard — by its key: the landing's\n * call-to-action shape. Whether the key names such a view is judged where\n * the page meets the App.\n */\nfunction parseHeadlineActions(raw: unknown, at: string): PageHeadlineAction[] {\n if (!Array.isArray(raw) || !raw.length || raw.length > 2) throw new Error(`${at}: \"links\" must be one or two { label, to }.`);\n return raw.map((entry, i) => {\n const here = `${at}, link ${i + 1}`;\n if (!entry || typeof entry !== 'object' || Array.isArray(entry)) throw new Error(`${here} is not an object { label, to }.`);\n const a = entry as Record<string, unknown>;\n if (a.to !== undefined && typeof a.to !== 'string') {\n allowKeys(a, ['label', 'to', 'intent'], here, INTENT_RENAMED);\n if (typeof a.label !== 'string' || !a.label.trim() || a.label.trim().length > 60)\n throw new Error(`${here} needs a \"label\" of at most 60 characters.`);\n if (a.intent !== undefined && a.intent !== 'primary' && a.intent !== 'secondary')\n throw new Error(`${here} has an \"intent\" of \"${String(a.intent)}\". Use \"primary\" or \"secondary\".`);\n return {\n label: a.label.trim(),\n to: parseAppTarget(a.to, `${here}.to`, true),\n ...(a.intent === undefined ? {} : { intent: a.intent as 'primary' | 'secondary' }),\n };\n }\n allowKeys(a, ['label', 'to', 'intent', 'prefill', 'defaults', 'open'], here, INTENT_RENAMED);\n if (typeof a.label !== 'string' || !a.label.trim() || a.label.trim().length > 60)\n throw new Error(`${here} needs a \"label\" of at most 60 characters.`);\n if (typeof a.to !== 'string' || !/^[a-z0-9][a-z0-9-]{0,63}$/.test(a.to))\n throw new Error(`${here} needs a \"to\": the key of one of the App's views, or an appLink().`);\n if (a.intent !== undefined && a.intent !== 'primary' && a.intent !== 'secondary')\n throw new Error(`${here} has an \"intent\" of \"${String(a.intent)}\". Use \"primary\" or \"secondary\".`);\n /* A public form over the record, rather than a page of its own. Only\n 'modal': \"drawer\" is a list's word for a row, and a page is the default\n because it is what an address opens. */\n if (a.open !== undefined && a.open !== 'modal')\n throw new Error(`${here} has an \"open\" of \"${String(a.open)}\". Use \"modal\", or leave it out for a page.`);\n /* Where the form the action opens starts: an input filled from this\n record (input → field path), or with the author's words — `defaults`, the\n word a New form and a related list's create say a stated starting value with. */\n const inputs = (raw: unknown, key: 'prefill' | 'defaults'): Record<string, string> | undefined => {\n if (raw === undefined) return undefined;\n if (!raw || typeof raw !== 'object' || Array.isArray(raw) || !Object.keys(raw).length)\n throw new Error(`${here} has a \"${key}\" that is not an object of form input → ${key === 'prefill' ? 'field path' : 'text'}.`);\n return Object.fromEntries(\n Object.entries(raw as Record<string, unknown>).map(([input, value]) => {\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(input)) throw new Error(`${here}, ${key}: \"${input}\" is not a form input's name.`);\n if (key === 'prefill' && (typeof value !== 'string' || !FIELD_PATH.test(value.trim())))\n throw new Error(`${here}, prefill: \"${input}\" must name one of this record's fields.`);\n if (key === 'defaults' && (typeof value !== 'string' || !value.trim() || value.length > 500))\n throw new Error(`${here}, defaults: \"${input}\" must be text of at most 500 characters.`);\n return [input, (value as string).trim()];\n }),\n );\n };\n const prefill = inputs(a.prefill, 'prefill');\n const defaults = inputs(a.defaults, 'defaults');\n return {\n label: a.label.trim(),\n view: a.to,\n ...(a.intent === undefined ? {} : { intent: a.intent as 'primary' | 'secondary' }),\n ...(prefill ? { prefill } : {}),\n ...(defaults ? { defaults } : {}),\n ...(a.open === undefined ? {} : { open: 'modal' as const }),\n };\n });\n}\n\nfunction parseWidth(raw: unknown, at: string): SectionWidth | undefined {\n if (raw === undefined) return undefined;\n const width = String(raw);\n if (!WIDTHS.includes(width as SectionWidth)) {\n throw new Error(`${at} has a \"width\" of \"${width}\". Use ${WIDTHS.join(', ')}.`);\n }\n return width as SectionWidth;\n}\n\nfunction parseOffset(raw: unknown, at: string, side: string): SectionOffset | undefined {\n if (raw === undefined) return undefined;\n const offset = String(raw);\n if (!OFFSETS.includes(offset as SectionOffset)) {\n throw new Error(`${at} has an \"${side}\" of \"${offset}\". Use ${OFFSETS.join(', ')}.`);\n }\n return offset as SectionOffset;\n}\n\nfunction parseSort(raw: unknown, at: string): PageList['orderBy'] {\n return parseSortText(raw, at, 'due_on desc, name');\n}\n","import type { AppForm, FormLines } from './app.js';\nimport { columnKind, isFlagColumn, type ColumnMeta, type DatabaseSchema, type TableMeta } from '@mistriwork/engine/shared/schema.js';\nimport { autoKey, formShownFields, isRequired } from '@mistriwork/engine/shared/form.js';\nimport { unknownKey } from '@mistriwork/engine/shared/suggest.js';\nimport { computedColumnMeta } from '@mistriwork/engine/shared/computed.js';\nimport {\n LOCAL_FIELD,\n RESERVED,\n columnNamed,\n compileCondition,\n comparisonResult,\n conditionResult,\n evaluateFormCondition,\n family,\n formDefaultWord,\n normalize,\n rowValue,\n scalar,\n type CompiledCondition,\n type FormCondition,\n type FormOperator,\n type FormScalar,\n type FormStep,\n type FormStyle,\n} from '@mistriwork/engine/shared/formValues.js';\nexport {\n MAX_FORM_CONDITION,\n MAX_FORM_PREDICATES,\n evaluateFormCondition,\n formDefaultWord,\n inferredFormDefaults,\n type FormCondition,\n type FormScalar,\n type FormStep,\n type FormStyle,\n} from '@mistriwork/engine/shared/formValues.js';\nimport type { AppFormField, FormChildGroup } from '@mistriwork/engine/shared/servedApp.js';\nexport type { AppFormField, FormChildGroup } from '@mistriwork/engine/shared/servedApp.js';\n\n/** A validation rule: the record must meet `must`, a condition, wherever `when` (a condition too) holds. The\n * message is shown at the first column `must` names. */\nexport interface FormRule {\n must: FormCondition;\n when?: FormCondition;\n message: string;\n}\n\nexport const FORM_OPEN = ['drawer', 'page'] as const;\nexport const FORM_STYLES = ['stepper', 'intake'] as const;\n/** How a vocabulary or a flag may be drawn on a New form (`fields.<column>.as`). */\nconst FORM_FIELD_AS = ['cards', 'buttons', 'radios', 'select'] as const;\nexport const FORM_FIELD_KEYS = ['label', 'help', 'placeholder', 'multiline', 'as', 'where'] as const;\n/** A narrowed reference compares at most this many columns. */\nexport const MAX_FORM_WHERE = 4;\nconst MAX_FORM_DESCRIPTION = 500;\n\nexport const MAX_FORM_RULES = 128;\nexport const MAX_FORM_FIELDS = 256;\nexport const MAX_FORM_GROUPS = 32;\nexport const MAX_FORM_CHILDREN = 16;\nexport const MAX_FORM_CHILD_ROWS = 64;\nconst MAX_FORM_CHILD_ID = 64;\nexport const MAX_FORM_STEPS = 16;\nexport const MAX_FORM_STEP_FIELDS = 256;\nconst MAX_FORM_STEP_ID = 64;\nexport const FORM_UNRESOLVED_MESSAGE = 'This value could not be validated.';\n// eslint-disable-next-line no-control-regex -- Resource names must exclude ASCII control characters.\nconst RESOURCE = /^\\S(?:[^\\x00-\\x1f\\x7f]*\\S)?$/u;\n\nfunction fail(where: string): never {\n // Never stringify rejected data: it may contain row values or executable hooks.\n throw new Error(`${where}: invalid form declaration.`);\n}\n\nfunction frozen<T extends object>(value: T): T {\n return Object.freeze(value);\n}\nfunction object(raw: unknown, where: string, keys?: readonly string[]): Record<string, unknown> {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return fail(where);\n const proto = Object.getPrototypeOf(raw);\n if (proto !== Object.prototype && proto !== null) return fail(where);\n const descriptors = Object.getOwnPropertyDescriptors(raw);\n if (Reflect.ownKeys(raw).length !== Object.keys(descriptors).length) return fail(where);\n const result: Record<string, unknown> = {};\n for (const [key, descriptor] of Object.entries(descriptors)) {\n /* An unknown key is named with the keys the place takes, as allowKeys\n names one everywhere else. A key is authored; the values are what\n fail() must never echo. */\n if (keys && !keys.includes(key) && !RESERVED.has(key)) throw new Error(`${where} ${unknownKey(key, keys)}.`);\n if (RESERVED.has(key) || !descriptor.enumerable || !Object.hasOwn(descriptor, 'value') || (keys && !keys.includes(key)))\n return fail(where);\n Object.defineProperty(result, key, { value: descriptor.value, enumerable: true });\n }\n return result;\n}\n\nfunction list(raw: unknown, where: string, max: number, min = 0): unknown[] {\n if (!Array.isArray(raw) || raw.length < min || raw.length > max || Object.getPrototypeOf(raw) !== Array.prototype) return fail(where);\n const descriptors = Object.getOwnPropertyDescriptors(raw);\n if (Reflect.ownKeys(raw).length !== raw.length + 1) return fail(where);\n const result: unknown[] = [];\n for (let i = 0; i < raw.length; i++) {\n const descriptor = descriptors[String(i)];\n if (!descriptor || !descriptor.enumerable || !Object.hasOwn(descriptor, 'value')) return fail(where);\n result.push(descriptor.value);\n }\n return result;\n}\n\nfunction field(raw: unknown, where: string): string {\n if (typeof raw !== 'string' || !LOCAL_FIELD.test(raw) || RESERVED.has(raw)) return fail(where);\n return raw;\n}\n\nfunction text(raw: unknown, where: string): string {\n // eslint-disable-next-line no-control-regex -- Static labels/messages must not contain ASCII control characters.\n if (typeof raw !== 'string' || !raw.trim() || raw.length > 256 || /[\\x00-\\x1f\\x7f]/u.test(raw)) return fail(where);\n return raw;\n}\n\nfunction childId(raw: unknown, where: string): string {\n if (typeof raw !== 'string' || raw.length > MAX_FORM_CHILD_ID || !/^[a-z][a-z0-9_-]*$/u.test(raw)) return fail(where);\n return raw;\n}\n\nfunction stepId(raw: unknown, where: string): string {\n if (typeof raw !== 'string' || raw.length > MAX_FORM_STEP_ID || !/^[a-z][a-z0-9_-]*$/u.test(raw)) return fail(where);\n return raw;\n}\n\n/** The id `step()` gives a step: its label's slug. */\nfunction stepSlug(label: string): string {\n return label\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-|-$/g, '');\n}\n\n/* A step written as an object may leave its id out, as `step()` does; the\n label's slug is its id then — a step that lists fields and one that saves\n through its own command alike (appSavedForms.ts), and a label with no slug that can be one\n (`'1. Basics'`) says what to write rather than failing opaquely. */\nexport function stepIdOf(s: Record<string, unknown>, label: string, where: string): string {\n if (Object.hasOwn(s, 'id')) return stepId(s.id, where);\n const slug = stepSlug(label);\n if (!/^[a-z][a-z0-9_-]*$/u.test(slug) || slug.length > MAX_FORM_STEP_ID) {\n throw new Error(`${where}: step \"${label}\" needs an \"id\" — lowercase letters, digits, \"_\" and \"-\", starting with a letter.`);\n }\n return slug;\n}\n\n/** Author prose that is not a row value: one sentence or a few, plain text. */\nfunction sentence(raw: unknown, where: string, max: number): string {\n // eslint-disable-next-line no-control-regex -- Static copy must not contain ASCII control characters.\n if (typeof raw !== 'string' || !raw.trim() || raw.length > max || /[\\x00-\\x1f\\x7f]/u.test(raw)) {\n throw new Error(`${where} is not plain text of at most ${max} characters.`);\n }\n return raw.trim();\n}\n\nfunction parseFormField(raw: unknown, name: string, where: string): AppFormField {\n const here = `${where}, fields \"${name}\"`;\n const f = object(raw, here, FORM_FIELD_KEYS);\n const result: AppFormField = {};\n for (const key of ['label', 'help', 'placeholder'] as const) {\n if (Object.hasOwn(f, key)) result[key] = sentence(f[key], `${here}, \"${key}\"`, 256);\n }\n if (Object.hasOwn(f, 'multiline')) {\n if (typeof f.multiline !== 'boolean') throw new Error(`${here}, \"multiline\" is true or false.`);\n result.multiline = f.multiline;\n }\n if (Object.hasOwn(f, 'as')) {\n if (typeof f.as !== 'string' || !FORM_FIELD_AS.includes(f.as as (typeof FORM_FIELD_AS)[number])) {\n throw new Error(`${here}, \"as\" is one of ${FORM_FIELD_AS.map((w) => `\"${w}\"`).join(', ')}.`);\n }\n result.as = f.as as (typeof FORM_FIELD_AS)[number];\n }\n if (Object.hasOwn(f, 'where')) {\n const pairs = Object.entries(object(f.where, `${here}, \"where\"`));\n if (!pairs.length || pairs.length > MAX_FORM_WHERE) {\n throw new Error(`${here}, \"where\" names one to ${MAX_FORM_WHERE} columns: { project_id: 'project_id' }.`);\n }\n result.where = frozen(Object.fromEntries(pairs.map(([target, local]) => [field(target, here), field(local, here)])));\n }\n return frozen(result);\n}\n\n/** A condition, checked as the query language and kept as its text. */\nexport function parseFormCondition(raw: unknown, where = 'Condition', paths = false): FormCondition {\n compileCondition(raw, where, undefined, 'record', paths);\n return (raw as string).trim();\n}\n\nexport function parseFormRules(raw: unknown, where = 'Validation'): FormRule[] {\n return frozen(\n list(raw, where, MAX_FORM_RULES).map((rawRule) => {\n const r = object(rawRule, where, ['must', 'when', 'message']);\n if (!Object.hasOwn(r, 'must')) return fail(where);\n return frozen({\n must: parseFormCondition(r.must, `${where}, \"must\"`, true),\n message: text(r.message, where),\n ...(Object.hasOwn(r, 'when') ? { when: parseFormCondition(r.when, `${where}, \"when\"`, true) } : {}),\n });\n }),\n );\n}\n\n/** The column a rule's message is shown at: the first one its `must` compares, or the reference it reads through. */\nfunction ruleField(rule: FormRule): string {\n return compileCondition(rule.must, 'Validation', undefined, 'record', true)[0][0].field.split('.')[0];\n}\n\nexport function parseAppValidation(raw: unknown, where = 'Validation'): Record<string, FormRule[]> {\n const declarations = object(raw, where);\n if (Object.keys(declarations).length > MAX_FORM_FIELDS) return fail(where);\n return frozen(\n Object.fromEntries(\n Object.entries(declarations).map(([resource, rules]) => {\n if (!RESOURCE.test(resource) || resource.length > 256) return fail(where);\n return [resource, parseFormRules(rules, where)];\n }),\n ),\n );\n}\n\n/** The most fields side by side in one row. */\nconst MAX_FORM_ROW = 3;\nconst STEP_WORDS = { title: 160, lead: 500, next: 40, skip: 40 } as const;\n/** The keys a step takes beside its id, label and fields. */\nconst STEP_WORD_KEYS = Object.keys(STEP_WORDS) as readonly (keyof typeof STEP_WORDS)[];\n\n/** A step's own words for the intake style: its heading, the sentence under it, and its button's. */\nexport function parseStepWords(\n s: Record<string, unknown>,\n where: string,\n keys: readonly (keyof typeof STEP_WORDS)[] = STEP_WORD_KEYS,\n): Pick<FormStep, 'title' | 'lead' | 'next' | 'skip'> {\n const result: Pick<FormStep, 'title' | 'lead' | 'next' | 'skip'> = {};\n for (const key of keys) if (Object.hasOwn(s, key)) result[key] = sentence(s[key], `${where}, \"${key}\"`, STEP_WORDS[key]);\n return result;\n}\n\n/**\n * Fields as authored, for a step, a group or the form's `show`: names, and inner lists that are rows\n * (`[['first_name', 'last_name'], 'dob']`). The fields are the names read through, in order, the list everything\n * else reads; `rows` is kept beside them only where some row holds more than one.\n */\nfunction parseRowFields(raw: unknown, where: string, max: number, min: number): { fields: string[]; rows?: string[][] } {\n const rows = list(raw, where, max, min).map((entry, i): string[] => {\n if (!Array.isArray(entry)) return [field(entry, where)];\n const at = `${where}, field ${i + 1}`;\n if (!entry.length) throw new Error(`${at} is an empty row. A row holds one to ${MAX_FORM_ROW} fields.`);\n if (entry.length > MAX_FORM_ROW)\n throw new Error(\n `${at} is a row of ${entry.length} fields; a row holds at most ${MAX_FORM_ROW}, side by side. Split it into two rows.`,\n );\n if (entry.some((member) => Array.isArray(member)))\n throw new Error(`${at} nests a row inside a row. A row is one list of field names: [['first_name', 'last_name'], 'dob'].`);\n return list(entry, at, MAX_FORM_ROW, 1).map((member) => field(member, where));\n });\n const fields = rows.flat();\n if (fields.length > max) return fail(where);\n const twice = fields.find((name, i) => fields.indexOf(name) !== i);\n if (twice !== undefined) throw new Error(`${where} names \"${twice}\" twice; a field stands in one place on the form.`);\n return {\n fields: frozen(fields),\n ...(rows.some((row) => row.length > 1) ? { rows: frozen(rows.map((row) => frozen(row))) } : {}),\n };\n}\n\n/**\n * A list read again from a parsed model: its flat `fields` and the `rows` kept beside them, which must say the same\n * names in the same order. Authored, the rows are the list itself.\n */\nfunction rowFields(\n raw: Record<string, unknown>,\n key: string,\n rowsKey: string,\n where: string,\n parsed: boolean,\n max: number,\n min: number,\n): { fields: string[]; rows?: string[][] } {\n if (!parsed || !Object.hasOwn(raw, rowsKey)) return parseRowFields(raw[key], where, max, min);\n const read = parseRowFields(raw[rowsKey], where, max, min);\n const flat = list(raw[key], where, max, min);\n if (flat.length !== read.fields.length || flat.some((name, i) => name !== read.fields[i])) return fail(where);\n return read;\n}\n\n/** The keys an authored resource form takes (children stay behind the shared parser). */\nconst FORM_KEYS = [\n 'show',\n 'defaults',\n 'derived',\n 'groups',\n 'when',\n 'steps',\n 'style',\n 'description',\n 'open',\n 'fields',\n 'lines',\n 'live',\n] as const;\n/** What an author writes: `derived` is read out of `defaults` (`{ from: 'service_id.price' }`), never said itself. */\nexport const AUTHORED_FORM_KEYS = FORM_KEYS.filter((key) => key !== 'derived');\n/** A value taken from a referenced record: one reference of this table, then a column of the table it names. */\nconst DERIVED_PATH = /^[A-Za-z_][A-Za-z0-9_]{0,127}\\.[A-Za-z_][A-Za-z0-9_]{0,127}$/;\n\n/** The most collections of child rows one New form asks, and the most names it says live. */\nconst MAX_FORM_LINES = 4;\nconst MAX_FORM_LIVE = 16;\nconst LINE_KEYS = ['label', 'from', 'whose', 'show', 'order', 'min', 'max'] as const;\nconst IDENT = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;\n\n/**\n * A New form's child rows — an order's lines, a bundle's items — asked as a\n * grid under the record's fields and inserted with it, in its transaction:\n * `lines: [{ label: 'Items', from: 'order_line', whose: 'order_id', show: ['product_id', 'qty', 'unit_price'] }]`.\n * The words are a related list's (`related(title, from, whose, { show })`), and its caption is a `label` as a group's is;\n * the catalog checks them where the form meets it (formLines.ts).\n */\nfunction parseFormLines(raw: unknown, where: string): FormLines[] {\n if (!Array.isArray(raw) || !raw.length || raw.length > MAX_FORM_LINES)\n throw new Error(\n `${where}: form.lines is a list of one to ${MAX_FORM_LINES} collections of child rows, each { label, from, whose, show }.`,\n );\n const seen = new Set<string>();\n return frozen(\n raw.map((entry, i) => {\n const at = `${where}: form.lines ${i + 1}`;\n if (!entry || typeof entry !== 'object' || Array.isArray(entry)) throw new Error(`${at} is not { label, from, whose, show }.`);\n const l = entry as Record<string, unknown>;\n if (l.title !== undefined) throw new Error(`${at} has \"title\", which is now \"label\", as a group's caption is.`);\n const extra = Object.keys(l).find((key) => !(LINE_KEYS as readonly string[]).includes(key));\n if (extra !== undefined) throw new Error(`${at} ${unknownKey(extra, LINE_KEYS)}.`);\n if (typeof l.label !== 'string' || !l.label.trim() || l.label.length > 80)\n throw new Error(`${at} needs a \"label\" of at most 80 characters.`);\n for (const key of ['from', 'whose'] as const)\n if (typeof l[key] !== 'string' || !IDENT.test(l[key] as string))\n throw new Error(\n `${at} needs \"${key}\": ${key === 'from' ? 'the resource of the rows' : 'the column of those rows that points at this record'}.`,\n );\n if (\n !Array.isArray(l.show) ||\n !l.show.length ||\n l.show.length > 12 ||\n l.show.some((column) => typeof column !== 'string' || !IDENT.test(column)) ||\n new Set(l.show).size !== l.show.length\n )\n throw new Error(`${at} needs \"show\": one to 12 of the row's own columns, each once.`);\n if (l.order !== undefined && (typeof l.order !== 'string' || !IDENT.test(l.order)))\n throw new Error(`${at} has an \"order\" that is not a column of the rows: the number each row's place is kept in.`);\n const bound = (key: 'min' | 'max') => {\n const v = l[key];\n if (v === undefined) return undefined;\n if (typeof v !== 'number' || !Number.isInteger(v) || v < (key === 'max' ? 1 : 0) || v > MAX_FORM_CHILD_ROWS)\n throw new Error(`${at} has a \"${key}\" that is not a whole number of rows up to ${MAX_FORM_CHILD_ROWS}.`);\n return v;\n };\n const min = bound('min'),\n max = bound('max');\n if (min !== undefined && max !== undefined && min > max) throw new Error(`${at} has a \"min\" above its \"max\".`);\n if (seen.has(l.from as string))\n throw new Error(`${at} asks the rows of \"${l.from as string}\" a second time; one collection per resource.`);\n seen.add(l.from as string);\n return frozen({\n label: l.label.trim(),\n from: l.from as string,\n whose: l.whose as string,\n show: frozen([...(l.show as string[])]),\n ...(l.order === undefined ? {} : { order: l.order as string }),\n ...(min === undefined ? {} : { min }),\n ...(max === undefined ? {} : { max }),\n });\n }),\n );\n}\n\n/** The resource's computed columns and rollups a New form says as its fields and lines are typed: `live: ['subtotal', 'total']`. */\nfunction parseFormLive(raw: unknown, where: string): string[] {\n if (\n !Array.isArray(raw) ||\n !raw.length ||\n raw.length > MAX_FORM_LIVE ||\n raw.some((name) => typeof name !== 'string' || !IDENT.test(name)) ||\n new Set(raw).size !== raw.length\n )\n throw new Error(`${where}: form.live is a list of one to ${MAX_FORM_LIVE} of the resource's computed columns and rollups, each once.`);\n return frozen([...(raw as string[])]);\n}\n\n/** `parsed`: the form is a model already read (its `show` derived from its groups), not an authored one. */\nfunction parseAppFormShape(raw: unknown, where: string, parsed = false): Omit<AppForm, 'children'> {\n /* A parsed model carries its rows beside its flat lists (`showRows`, a group's and a step's `rows`); an author writes\n them inside the lists. */\n const f = object(raw, where, parsed ? [...FORM_KEYS, 'showRows'] : FORM_KEYS);\n const result: AppForm = {};\n /* `show` is what the form asks. Its steps, or else its groups, are that list already, so saying both is two lists to keep\n alike: it is the one or the other. */\n if (Object.hasOwn(f, 'show')) {\n if (parsed && Object.hasOwn(f, 'groups') && !Object.hasOwn(f, 'steps')) {\n /* derived again below */\n } else if (Object.hasOwn(f, 'steps') || Object.hasOwn(f, 'groups')) {\n throw new Error(\n `${where} has \"show\" and ${Object.hasOwn(f, 'steps') ? 'steps' : 'groups'}: the form asks the fields of its ${Object.hasOwn(f, 'steps') ? 'steps' : 'groups'}. Keep one.`,\n );\n }\n const { fields, rows } = rowFields(f, 'show', 'showRows', where, parsed, MAX_FORM_FIELDS, 0);\n result.show = fields;\n if (rows) result.showRows = rows;\n }\n if (Object.hasOwn(f, 'defaults')) {\n const defaults = object(f.defaults, where);\n if (Object.keys(defaults).length > MAX_FORM_FIELDS) return fail(where);\n result.defaults = frozen(Object.fromEntries(Object.entries(defaults).map(([key, value]) => [field(key, where), scalar(value, where)])));\n }\n if (Object.hasOwn(f, 'derived')) {\n const derived = object(f.derived, where);\n if (Object.keys(derived).length > MAX_FORM_FIELDS) return fail(where);\n result.derived = frozen(\n Object.fromEntries(\n Object.entries(derived).map(([key, path]) => {\n if (typeof path !== 'string' || !DERIVED_PATH.test(path))\n throw new Error(\n `${where}: defaults \"${key}\" is from ${JSON.stringify(path)}; a value from a record is { from: 'reference.column' }.`,\n );\n if (Object.hasOwn(result.defaults ?? {}, key)) throw new Error(`${where}: defaults \"${key}\" is said twice.`);\n return [field(key, where), path];\n }),\n ),\n );\n }\n if (Object.hasOwn(f, 'groups')) {\n const seen = new Set<string>();\n result.groups = frozen(\n list(f.groups, where, MAX_FORM_GROUPS).map((rawGroup) => {\n const g = object(rawGroup, where, parsed ? ['label', 'description', 'fields', 'rows'] : ['label', 'description', 'fields']);\n const { fields, rows } = rowFields(g, 'fields', 'rows', where, parsed, MAX_FORM_FIELDS, 1);\n for (const name of fields) {\n if (seen.has(name)) return fail(where);\n seen.add(name);\n }\n const label = text(g.label, where);\n return frozen({\n label,\n ...(Object.hasOwn(g, 'description')\n ? { description: sentence(g.description, `${where}, group \"${label}\", \"description\"`, 256) }\n : {}),\n fields,\n ...(rows ? { rows } : {}),\n });\n }),\n );\n }\n /* The groups carve up what the form asks, in their order; with steps, the steps' fields are the list (formShownFields). */\n if (result.groups && !Object.hasOwn(f, 'steps')) {\n result.show = frozen(result.groups.flatMap((group) => [...group.fields]));\n /* Their rows are the form's, read in the same order; a group without rows is one field to a row. */\n if (result.groups.some((group) => group.rows))\n result.showRows = frozen(result.groups.flatMap((group) => group.rows ?? group.fields.map((name) => frozen([name]))));\n }\n if (Object.hasOwn(f, 'when')) {\n const conditions = object(f.when, where);\n if (Object.keys(conditions).length > MAX_FORM_FIELDS) return fail(where);\n result.when = frozen(\n Object.fromEntries(\n Object.entries(conditions).map(([key, value]) => {\n const name = field(key, where);\n if (result.show && !result.show.includes(name)) return fail(where);\n return [name, parseFormCondition(value, `${where}, when \"${name}\"`)];\n }),\n ),\n );\n }\n if (Object.hasOwn(f, 'steps')) {\n const seenIds = new Set<string>();\n const owned = new Set<string>();\n result.steps = frozen(\n list(f.steps, where, MAX_FORM_STEPS, 1).map((rawStep) => {\n const s = object(rawStep, where, ['id', 'label', 'fields', ...(parsed ? ['rows'] : []), ...STEP_WORD_KEYS]);\n const label = text(s.label, where);\n const id = stepIdOf(s, label, where);\n if (seenIds.has(id)) throw new Error(`${where}: two steps are \"${id}\"; give one an \"id\" of its own.`);\n seenIds.add(id);\n const { fields, rows } = rowFields(s, 'fields', 'rows', where, parsed, MAX_FORM_STEP_FIELDS, 1);\n if (fields.some((name) => owned.has(name))) return fail(where);\n fields.forEach((name) => owned.add(name));\n return frozen({ id, label, fields, ...(rows ? { rows } : {}), ...parseStepWords(s, `${where}, step \"${id}\"`) });\n }),\n );\n /* A row stands inside one group, as a group stands inside one step: the group's heading sits over the row, and\n the fields a group leaves out are drawn after every group. */\n if (result.groups) {\n const groupOf = new Map(result.groups.flatMap((group, at) => group.fields.map((name) => [name, at] as const)));\n for (const step of result.steps)\n for (const row of step.rows ?? []) {\n if (new Set(row.map((name) => groupOf.get(name) ?? -1)).size > 1)\n throw new Error(\n `${where}: step \"${step.id}\" puts ${row.map((name) => `\"${name}\"`).join(', ')} in one row across groups. A row stands inside one group.`,\n );\n }\n }\n }\n if (Object.hasOwn(f, 'style')) {\n if (!(FORM_STYLES as readonly unknown[]).includes(f.style)) throw new Error(`${where}, \"style\" is 'stepper' or 'intake'.`);\n if (!Object.hasOwn(f, 'steps')) throw new Error(`${where}, \"style\" draws steps, and this form has none.`);\n result.style = f.style as FormStyle;\n }\n if (Object.hasOwn(f, 'description')) result.description = sentence(f.description, `${where}, \"description\"`, MAX_FORM_DESCRIPTION);\n if (Object.hasOwn(f, 'open')) {\n if (!(FORM_OPEN as readonly unknown[]).includes(f.open)) throw new Error(`${where}, \"open\" is 'drawer' or 'page'.`);\n result.open = f.open as AppForm['open'];\n }\n if (Object.hasOwn(f, 'lines')) result.lines = parseFormLines(f.lines, where);\n if (Object.hasOwn(f, 'live')) result.live = parseFormLive(f.live, where);\n if (Object.hasOwn(f, 'fields')) {\n const declared = Object.entries(object(f.fields, `${where}, \"fields\"`));\n if (declared.length > MAX_FORM_FIELDS) return fail(where);\n result.fields = frozen(Object.fromEntries(declared.map(([name, value]) => [field(name, where), parseFormField(value, name, where)])));\n }\n return frozen(result);\n}\n\nfunction parseFormChildren(raw: unknown, where = 'Form, children'): FormChildGroup[] {\n const seen = new Set<string>();\n return frozen(\n list(raw, where, MAX_FORM_CHILDREN).map((rawChild) => {\n const c = object(rawChild, where, ['id', 'label', 'table', 'relation', 'min', 'max', 'form']);\n const id = childId(c.id, where);\n if (seen.has(id)) return fail(where);\n seen.add(id);\n const relation = object(c.relation, where, ['from', 'to']);\n const min = c.min === undefined ? 0 : c.min;\n const max = c.max;\n if (\n typeof min !== 'number' ||\n typeof max !== 'number' ||\n !Number.isInteger(min) ||\n min < 0 ||\n min > MAX_FORM_CHILD_ROWS ||\n !Number.isInteger(max) ||\n max < 1 ||\n max > MAX_FORM_CHILD_ROWS ||\n min > max\n )\n return fail(where);\n if (typeof c.table !== 'string' || !RESOURCE.test(c.table) || c.table.length > 256) return fail(where);\n const captured: FormChildGroup = {\n id,\n label: text(c.label, where),\n table: c.table,\n relation: { from: field(relation.from, where), to: field(relation.to, where) },\n ...(min ? { min } : {}),\n max,\n ...(c.form === undefined ? {} : { form: parseAppFormShape(c.form, `${where} \"${id}\", form`) }),\n };\n return frozen(captured);\n }),\n );\n}\n\nexport function parseAppForm(raw: unknown, where = 'Form', parsed = false): AppForm {\n const input = object(raw, where, [...FORM_KEYS, ...(parsed ? ['showRows'] : []), 'children']);\n const shape = parseAppFormShape(Object.fromEntries(Object.entries(input).filter(([key]) => key !== 'children')), where, parsed);\n if (!Object.hasOwn(input, 'children')) return shape as AppForm;\n return frozen({ ...shape, children: parseFormChildren(input.children, `${where}, children`) });\n}\n\nexport function referencedConditionFields(condition: FormCondition, paths = false): string[] {\n return [\n ...new Set(\n compileCondition(condition, 'Condition', undefined, 'record', paths)\n .flat()\n .map((p) => p.field),\n ),\n ];\n}\n\nexport function referencedRuleFields(rules: readonly FormRule[]): string[] {\n return [\n ...new Set(\n parseFormRules(rules).flatMap((rule) => [\n ...compileCondition(rule.must, 'Validation', undefined, 'record', true)\n .flat()\n .flatMap((p) => [p.field, ...(p.other === undefined ? [] : [p.other])]),\n ...(rule.when ? referencedConditionFields(rule.when, true) : []),\n ]),\n ),\n ];\n}\n\n/** A record as a rule reads it: a text column holding only blanks is as empty as a missing one. */\nfunction blanksAsEmpty(row: Readonly<Record<string, unknown>>, columns: readonly ColumnMeta[]): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n for (const column of columns) {\n const value = rowValue(row, column.name);\n result[column.name] = typeof value === 'string' && !value.trim() && family(column) === 'text' ? null : value;\n }\n return result;\n}\n\nfunction comparisonProblem(column: ColumnMeta, op: FormOperator): boolean {\n const kind = family(column);\n return !kind || ((kind === 'text' || kind === 'boolean') && op !== 'eq' && op !== 'ne');\n}\n\nfunction conditionProblems(condition: FormCondition, table: TableMeta, paths = false): string[] {\n let compiled: CompiledCondition;\n try {\n compiled = compileCondition(\n condition,\n `Condition \"${condition}\"`,\n table.columns,\n /^\\w+$/.test(table.name) ? table.name : undefined,\n paths,\n );\n } catch (error) {\n return [(error as Error).message];\n }\n return compiled.flat().flatMap((p) => {\n const column = columnNamed(table.columns, p.field);\n if (p.other !== undefined) {\n const other = columnNamed(table.columns, p.other);\n return !column || !other || family(column) !== family(other) || comparisonProblem(column, p.operator)\n ? [`Condition on \"${p.field}\" is not comparable with \"${p.other}\" in this schema.`]\n : [];\n }\n return !column ||\n comparisonProblem(column, p.operator) ||\n p.values.some((value) => normalize(column, value) === undefined || (value === null && p.operator !== 'eq' && p.operator !== 'ne'))\n ? [`Condition on \"${p.field}\" is not comparable with this schema.`]\n : [];\n });\n}\n\n/** The `reference.column` paths a validation reads through its references, each once. */\nexport function ruleReferencePaths(rules: readonly FormRule[]): string[] {\n return referencedRuleFields(rules).filter((field) => field.includes('.'));\n}\n\n/**\n * The columns a validation is judged against: the table's own, and each column it reads through a reference,\n * named by its path and typed as the referenced column is. `problems` says each path the catalog cannot follow.\n */\nexport function ruleColumns(\n rules: readonly FormRule[],\n table: TableMeta,\n schema: DatabaseSchema | undefined,\n): { columns: ColumnMeta[]; problems: string[] } {\n const columns = [...table.columns];\n const problems: string[] = [];\n for (const path of ruleReferencePaths(rules)) {\n const source = derivedSource(path, table, schema);\n if ('problem' in source) problems.push(`Validation reads \"${path}\": ${source.problem}`);\n else columns.push({ ...source.column, name: path, primaryKey: false });\n }\n return { columns, problems };\n}\n\nexport function validateFormRules(rules: readonly FormRule[], table: TableMeta, schema?: DatabaseSchema): string[] {\n const captured = parseFormRules(rules);\n // Without a catalog a path cannot be followed, and is left to the check that has one.\n if (!schema && ruleReferencePaths(captured).length) return [];\n const { columns, problems } = ruleColumns(captured, table, schema);\n if (problems.length) return problems;\n const judged = { ...table, columns };\n for (const rule of captured) {\n problems.push(...conditionProblems(rule.must, judged, true));\n if (rule.when) problems.push(...conditionProblems(rule.when, judged, true));\n }\n return [...new Set(problems)];\n}\n\n/** What a form is judged against beyond its own table: the catalog its references\n * point into, and who `me` can be — `null` for an App without sign-in, the\n * `auth.me` table's id (or undefined, email only) where people sign in. */\ninterface AppFormContext {\n schema?: DatabaseSchema;\n /** The catalog a value from a referenced record is read from: `schema` with its computed columns (app.ts storedComputed). */\n derivedFrom?: DatabaseSchema;\n person?: { table?: string } | null;\n}\n\n/** Use the current readable projection; this helper never grants field access. */\nexport function validateAppForm(\n form: AppForm,\n table: TableMeta,\n readableFields: readonly string[] = table.columns.map((c) => c.name),\n context: AppFormContext = {},\n): string[] {\n const captured = parseAppForm(form, 'Form', true);\n const readable = new Set(readableFields);\n const stepFields = (captured.steps ?? []).flatMap((step) => step.fields);\n const shownFields = formShownFields(captured);\n const presentationFields = [\n ...(captured.show ?? []),\n ...Object.keys(captured.defaults ?? {}),\n ...(captured.groups ?? []).flatMap((group) => group.fields),\n ...Object.keys(captured.when ?? {}),\n ...stepFields,\n ...Object.keys(captured.fields ?? {}),\n ...Object.values(captured.fields ?? {}).flatMap((f) => Object.values(f?.where ?? {})),\n ];\n const problems: string[] = [];\n const defaults = captured.defaults ?? {};\n const named = [...(captured.show ?? []), ...Object.keys(defaults)].filter((name) => !columnNamed(table.columns, name));\n const assignedDefaults = table.columns.filter((c) => c.identity === 'always' && Object.hasOwn(defaults, c.name));\n const nullDefaults = table.columns.filter((c) => !c.nullable && Object.hasOwn(defaults, c.name) && defaults[c.name] === null);\n if (named.length) problems.push(`Form: \"${table.name}\" has no column ${named.map((name) => `\"${name}\"`).join(', ')}.`);\n if (assignedDefaults.length)\n problems.push(\n `Form: ${assignedDefaults.map((c) => `\"${c.name}\"`).join(', ')} is always assigned by the database and cannot have a form default.`,\n );\n if (nullDefaults.length) problems.push(`Form: ${nullDefaults.map((c) => `\"${c.name}\"`).join(', ')} cannot default to null (NOT NULL).`);\n for (const [name, path] of Object.entries(captured.derived ?? {}))\n problems.push(...derivedProblems(name, path, table, context.derivedFrom ?? context.schema));\n if (shownFields) {\n const shown = new Set([...shownFields, ...Object.keys(defaults), ...Object.keys(captured.derived ?? {})]);\n const assigned = autoKey(table);\n const starved = table.columns.filter((c) => isRequired(c, assigned) && !shown.has(c.name));\n if (starved.length)\n problems.push(\n `Form: \"${table.name}\" requires ${starved.map((c) => `\"${c.name}\"`).join(', ')} (NOT NULL, no default) — show it, or give it a default.`,\n );\n }\n if (captured.steps) {\n const shown = new Set(captured.show ?? stepFields);\n const owned = new Set(stepFields);\n const missingOwnership = [...shown].filter((name) => !owned.has(name));\n const unshown = [...owned].filter((name) => !shown.has(name));\n if (missingOwnership.length || unshown.length) {\n problems.push('Form steps must own exactly the fields in show.');\n }\n const owner = new Map<string, string>();\n for (const step of captured.steps) for (const name of step.fields) owner.set(name, step.id);\n for (const group of captured.groups ?? []) {\n const stepIds = new Set(group.fields.map((name) => owner.get(name)).filter((id): id is string => id !== undefined));\n if (stepIds.size > 1) problems.push(`Form group \"${group.label}\" cannot span multiple steps.`);\n if (group.fields.some((name) => !owner.has(name)))\n problems.push(`Form group \"${group.label}\" contains a field outside the form steps.`);\n }\n }\n for (const name of new Set(presentationFields)) {\n if (!columnNamed(table.columns, name) || !readable.has(name)) problems.push(`Form field \"${name}\" is unknown or hidden.`);\n }\n /* A field may be shown while a computed column holds: the form works it out from the draft as it is filled in\n (newRecordModel.ts), so the condition reads it as a column, as the browser does. */\n const computed = table.computed ?? {};\n const judged = Object.keys(computed).length\n ? { ...table, columns: [...table.columns, ...Object.entries(computed).map(([name, value]) => computedColumnMeta(name, value))] }\n : table;\n for (const condition of Object.values(captured.when ?? {})) {\n if (!condition) continue;\n problems.push(...conditionProblems(condition, judged));\n if (referencedConditionFields(condition).some((name) => !readable.has(name) && !Object.hasOwn(computed, name))) {\n problems.push('Form visibility depends on an unreadable field.');\n }\n }\n const shownNow = new Set(shownFields ?? table.columns.map((c) => c.name));\n for (const [name, dress] of Object.entries(captured.fields ?? {})) {\n const column = columnNamed(table.columns, name);\n if (!column || !dress) continue;\n if (!shownNow.has(name)) problems.push(`Form: fields \"${name}\" is not a field this form asks for; add it to show, or drop its dress.`);\n if (dress.multiline !== undefined && family(column) !== 'text')\n problems.push(`Form: fields \"${name}\" is ${column.type || 'untyped'}; only a text field is drawn on one line or several.`);\n if (dress.as !== undefined) {\n const vocabulary = Boolean(column.allowed?.length);\n if (!vocabulary && !isFlagColumn(column))\n problems.push(\n `Form: fields \"${name}\" is ${column.type || 'untyped'}; \"as\" draws a column with a vocabulary (an enum or a check-constrained text) or a flag.`,\n );\n else if (!vocabulary && (dress.as === 'cards' || dress.as === 'radios'))\n problems.push(\n `Form: fields \"${name}\" is a flag; \"as: '${dress.as}'\" needs a vocabulary, a flag is drawn as \"buttons\" or \"select\".`,\n );\n }\n if (dress.where) problems.push(...narrowingProblems(name, column, dress.where, table, context.schema));\n }\n for (const [name, value] of Object.entries(defaults)) {\n const column = columnNamed(table.columns, name);\n if (column) problems.push(...defaultWordProblems(name, column, value, context));\n }\n return [...new Set(problems)];\n}\n\n/* A narrowed picker is the reference's choices, where the referenced row\n agrees with this record on one to four columns. The server holds every\n write to it (appValidation.ts), so what it names must be real columns. */\nfunction narrowingProblems(\n name: string,\n column: ColumnMeta,\n where: Record<string, string>,\n table: TableMeta,\n schema?: DatabaseSchema,\n): string[] {\n if (!column.references)\n return [`Form: fields \"${name}\" narrows with \"where\", which only a reference takes; \"${name}\" refers to nothing.`];\n const problems: string[] = [];\n for (const local of Object.values(where)) {\n if (local === name) problems.push(`Form: fields \"${name}\", \"where\" compares \"${name}\" with itself.`);\n else if (!columnNamed(table.columns, local))\n problems.push(`Form: fields \"${name}\", \"where\" names \"${local}\", which is not a column of ${table.name}.`);\n }\n if (!schema) return problems;\n const target = schema.tables.find((candidate) => candidate.id === column.references!.table);\n if (!target)\n return [\n ...problems,\n `Form: fields \"${name}\" refers to ${column.references.table}, which this catalog does not read, so its choices cannot be narrowed.`,\n ];\n for (const remote of Object.keys(where)) {\n if (!columnNamed(target.columns, remote))\n problems.push(`Form: fields \"${name}\", \"where\" names \"${remote}\", which is not a column of ${target.name}.`);\n }\n return problems;\n}\n\nfunction referencedTable(to: string | undefined, schema: DatabaseSchema | undefined): TableMeta | undefined {\n return to && schema\n ? (schema.tables.find((candidate) => candidate.id === to) ?? schema.tables.find((candidate) => candidate.name === to))\n : undefined;\n}\n\n/** The table a reference column of `table` names, where the catalog reads it. */\nexport function referenceTarget(via: string, table: TableMeta, schema: DatabaseSchema | undefined): TableMeta | undefined {\n const reference = columnNamed(table.columns, via);\n const fk = schema?.foreignKeys.find((key) => key.from.table === table.id && key.from.columns.length === 1 && key.from.columns[0] === via);\n return reference ? referencedTable(reference.references?.table ?? fk?.to.table, schema) : undefined;\n}\n\n/**\n * A value from a referenced record (`defaults: { labor_charge: { from: 'service_id.price' } }`): the first name\n * is a reference of this table, the second a column of the table it names, holding what this column holds.\n */\nexport function derivedSource(\n path: string,\n table: TableMeta,\n schema: DatabaseSchema | undefined,\n): { via: ColumnMeta; target: TableMeta; column: ColumnMeta; key: string } | { problem: string } {\n const [via, name] = path.split('.');\n const reference = columnNamed(table.columns, via);\n if (!reference) return { problem: `\"${table.name}\" has no column \"${via}\".` };\n const fk = schema?.foreignKeys.find((key) => key.from.table === table.id && key.from.columns.length === 1 && key.from.columns[0] === via);\n const to = reference.references?.table ?? fk?.to.table;\n const target = referencedTable(to, schema);\n if (!to) return { problem: `\"${via}\" is no reference, so it names no record to take a value from.` };\n if (!target) return { problem: `\"${via}\" refers to ${to}, which this catalog does not read.` };\n const column = columnNamed(target.columns, name);\n if (!column) return { problem: `${target.name} has no column \"${name}\".` };\n const key = reference.references?.column ?? fk?.to.columns[0] ?? target.columns.find((candidate) => candidate.primaryKey)?.name;\n if (!key) return { problem: `${target.name} has no key \"${via}\" could name.` };\n return { via: reference, target, column, key };\n}\n\nfunction derivedProblems(name: string, path: string, table: TableMeta, schema: DatabaseSchema | undefined): string[] {\n const column = columnNamed(table.columns, name);\n if (!column) return [`Form: \"${table.name}\" has no column \"${name}\".`];\n if (!schema) return [];\n const source = derivedSource(path, table, schema);\n if ('problem' in source) return [`Form: defaults \"${name}\" is from \"${path}\": ${source.problem}`];\n if (columnKind(source.column.type) !== columnKind(column.type))\n return [\n `Form: defaults \"${name}\" is from \"${path}\", which is ${source.column.type || 'untyped'}; \"${name}\" is ${column.type || 'untyped'}.`,\n ];\n return [];\n}\n\nfunction defaultWordProblems(name: string, column: ColumnMeta, value: FormScalar, context: AppFormContext): string[] {\n if (value === 'today' || value === 'now') {\n return formDefaultWord(column, value)\n ? []\n : [\n `Form: defaults \"${name}\" is ${value}, which fills a date column; \"${name}\" is ${column.type || 'untyped'}` +\n \" (a SQLite TEXT date is declared with types: { column: 'date' }).\",\n ];\n }\n if (value !== 'me') return [];\n if (!formDefaultWord(column, value))\n return [\n `Form: defaults \"${name}\" is me, the signed-in person — a row of the auth.me table or an email; \"${name}\" is ${column.type || 'untyped'}.`,\n ];\n if (context.person === null) return [`Form: defaults \"${name}\" is me, and this App has no sign-in.`];\n if (context.person && column.references && column.references.table !== context.person.table) {\n return [\n `Form: defaults \"${name}\" is me, and \"${name}\" refers to ${column.references.table}, not to the table people are rows of (auth.me).`,\n ];\n }\n return [];\n}\n\nfunction tableId(table: TableMeta): string {\n return table.id;\n}\n\n/** Validate the child-group contract against a catalog before it is exposed.\n * This checks only shape, relation and field authority; it never creates a\n * child, infers a write grant, or treats a presentation form as permission. */\nexport function validateFormChildren(\n children: readonly FormChildGroup[],\n parent: TableMeta,\n schema: DatabaseSchema,\n readableFields: ReadonlyMap<string, readonly string[]> = new Map(),\n): string[] {\n const problems: string[] = [];\n const seen = new Set<string>();\n for (const child of parseFormChildren(children)) {\n if (seen.has(child.id)) problems.push(`Child group \"${child.id}\" is declared more than once.`);\n seen.add(child.id);\n const targets = schema.tables.filter((table) => table.id === child.table || table.name === child.table);\n const target = targets.length === 1 ? targets[0] : undefined;\n if (!target) {\n problems.push(`Child group \"${child.id}\" names an unknown or ambiguous table.`);\n continue;\n }\n const from = columnNamed(target.columns, child.relation.from);\n const to = columnNamed(parent.columns, child.relation.to);\n if (!from || !to) problems.push(`Child group \"${child.id}\" has an unknown relation column.`);\n const relation = schema.foreignKeys.filter(\n (fk) =>\n fk.from.table === tableId(target) &&\n fk.to.table === tableId(parent) &&\n fk.from.columns.length === 1 &&\n fk.to.columns.length === 1 &&\n fk.from.columns[0] === child.relation.from &&\n fk.to.columns[0] === child.relation.to,\n );\n if (relation.length !== 1) problems.push(`Child group \"${child.id}\" must name one declared foreign-key relation to its parent.`);\n const readable = readableFields.get(target.id) ?? target.columns.map((column) => column.name);\n if (child.form)\n problems.push(...validateAppForm(child.form, target, readable).map((problem) => `Child group \"${child.id}\": ${problem}`));\n }\n return [...new Set(problems)];\n}\n\ninterface FormChildInput {\n /** Child rows keyed by the authored group ID; absent means an empty draft. */\n rows?: readonly Readonly<Record<string, unknown>>[];\n}\n\ninterface FormChildrenResult {\n valid: boolean;\n errors: Record<string, string[]>;\n unresolved: string[];\n}\n\nfunction safeScalar(raw: unknown): FormScalar | undefined {\n if (raw === null) return null;\n if (typeof raw === 'string') return raw;\n if (typeof raw === 'boolean') return raw;\n if (typeof raw === 'number' && Number.isFinite(raw) && Math.abs(raw) <= Number.MAX_SAFE_INTEGER) return raw;\n return undefined;\n}\n\n/** Evaluate a bounded aggregate draft without persistence. The caller supplies\n * the already-authorized resource rules; unknown rows and relations remain\n * unresolved rather than becoming a false success. */\nexport function evaluateFormChildren(\n children: readonly FormChildGroup[],\n parentRow: Readonly<Record<string, unknown>>,\n inputs: Readonly<Record<string, FormChildInput | undefined>>,\n schema: DatabaseSchema,\n validation: ReadonlyMap<string, readonly FormRule[]> = new Map(),\n): FormChildrenResult {\n const errors: Record<string, string[]> = {};\n const unresolved = new Set<string>();\n const add = (key: string, message: string) => {\n const list = errors[key] ?? (errors[key] = []);\n if (!list.includes(message)) list.push(message);\n };\n for (const child of parseFormChildren(children)) {\n const targets = schema.tables.filter((table) => table.id === child.table || table.name === child.table);\n const target = targets.length === 1 ? targets[0] : undefined;\n if (!target) {\n unresolved.add(child.id);\n continue;\n }\n const input = inputs[child.id];\n const rows = input?.rows ?? [];\n if (!Array.isArray(rows) || rows.length > child.max || rows.length < (child.min ?? 0)) {\n add(child.id, `Add between ${child.min ?? 0} and ${child.max} items.`);\n continue;\n }\n const parentValue = safeScalar(rowValue(parentRow, child.relation.to));\n const fk = columnNamed(target.columns, child.relation.from);\n if (!fk || parentValue === undefined) {\n unresolved.add(child.id);\n continue;\n }\n const rules = validation.get(target.id) ?? validation.get(target.name) ?? [];\n rows.forEach((row, index) => {\n const key = `${child.id}[${index}]`;\n if (!row || typeof row !== 'object' || Array.isArray(row)) {\n unresolved.add(key);\n return;\n }\n const childValue = rowValue(row, child.relation.from);\n const linked = comparisonResult({ field: child.relation.from, operator: 'eq', values: [parentValue] }, row, target.columns);\n if (linked === false) add(`${key}.${child.relation.from}`, 'This item belongs to a different parent.');\n else if (linked === 'unknown' || childValue === undefined) unresolved.add(`${key}.${child.relation.from}`);\n if (rules.length) {\n const result = evaluateFormRules(rules, row, target.columns, { mode: 'authoritative' });\n for (const [fieldName, messages] of Object.entries(result.errors)) {\n for (const message of messages) add(`${key}.${fieldName}`, message);\n }\n result.unresolved.forEach((fieldName) => unresolved.add(`${key}.${fieldName}`));\n }\n });\n }\n return { valid: !Object.keys(errors).length && !unresolved.size, errors, unresolved: [...unresolved] };\n}\n\ninterface FormRuleResult {\n valid: boolean;\n errors: Record<string, string[]>;\n /** Rule target fields whose result is unknown, including incomplete drafts. */\n unresolved: string[];\n}\n\n/** Validate a complete candidate row, after trusted default resolution / update\n * merging. Does not read defaults, merge drafts, write, or grant authority.\n * Preview leaves unknown rules unresolved without showing premature field errors.\n * Authoritative mode rejects them with fixed safe messages. Both return valid:false.\n * Transport owners must also redact messages/field keys for unreadable targets.\n */\nexport function evaluateFormRules(\n rules: readonly FormRule[],\n row: Readonly<Record<string, unknown>>,\n columns: readonly ColumnMeta[],\n options?: { mode: 'authoritative' | 'preview' },\n): FormRuleResult {\n const errors: Record<string, string[]> = {};\n const unresolved = new Set<string>();\n const add = (name: string, message: string) => {\n const messages = errors[name] ?? (errors[name] = []);\n if (!messages.includes(message)) messages.push(message);\n };\n const judged = blanksAsEmpty(row, columns);\n for (const rule of parseFormRules(rules)) {\n const applies = rule.when ? evaluateFormCondition(rule.when, row, columns, true) : true;\n if (applies === false) continue;\n const at = ruleField(rule);\n let result: boolean | 'unknown' = 'unknown';\n if (applies === true) {\n try {\n result = conditionResult(compileCondition(rule.must, 'Validation', columns, 'record', true), judged, columns);\n } catch {\n result = 'unknown';\n }\n }\n if (result === 'unknown') {\n unresolved.add(at);\n if (options?.mode !== 'preview') add(at, FORM_UNRESOLVED_MESSAGE);\n } else if (!result) add(at, rule.message);\n }\n return { valid: !Object.keys(errors).length && !unresolved.size, errors, unresolved: [...unresolved] };\n}\n","/**\n * Flows whose steps are rows: a resource's `flows` binds a table of\n * flows and a table of their steps, which an administrator edits while the\n * App runs, to the durable workflow engine that runs them on its records:\n *\n * purchase_order: {\n * flows: {\n * from: 'po_flow', steps: 'po_flow_step',\n * set: ['notes'],\n * events: { 'po.decided': 'order' },\n * kinds: { approval: { command: 'requestApproval', input: ['approver'], waitFor: 'po.decided' } },\n * },\n * }\n *\n * The rows have fixed column names, so there is one way to write one. A flow\n * is `id`, `name` and `start` (`created`, `updated` or `manual`), and may\n * have a `condition` in the query language, `changes` (the one column an\n * update must change) and `active`. A step is `flow_id`, `position` and\n * `kind`, with the inputs its kind reads: the platform's own kinds (`wait`,\n * `until`, `branch`, `set`, `waitFor`, `notify`) and the App's, each of which runs one\n * of the App's commands on the record — so whatever a flow does to data goes\n * through the command door, under its rules, principal and audit.\n *\n * Steps that share a position start together, and the run goes on once each\n * is done. A step of an App kind that waits for an event may wait for `all`\n * of those its command asked (its `mode`), end early when its `condition`\n * holds, give up after its `duration`, and run the kind's `remind` command\n * every `remind` while it waits.\n *\n * A `notify` step tells an `audience` its `message`: the resource's `notify`\n * rules `on: { flow: '<audience>' }` say who that is and how (`to`, `send`,\n * `email`), and the words are the step's (`{message}` in the rule's title),\n * kept in the inbox in the step's own transaction (adapters/inbox/inbox.ts).\n *\n * Pure, and shared: the server judges a stored row here (appValidation.ts)\n * and compiles a flow's rows into the plan a run follows (flowCommand.ts)\n * with the same words, so a row the form accepted is a step the run can take.\n */\nimport { parseDuration } from '@mistriwork/engine/shared/duration.js';\n\n/** A step kind the App declares: the command it runs on the record, the step columns it takes as input, and an event to wait for after. */\ninterface FlowKind {\n readonly command: string;\n /** Step columns passed to the command under their own names; `'note?'` is one a step may leave empty. */\n readonly input?: readonly string[];\n /** An event of `events` the run waits for once the command has run. */\n readonly waitFor?: string;\n /** A command run on the record, with the same input, every `remind` a step of this kind waits. */\n readonly remind?: string;\n}\n\n/** `resources.<r>.flows`: the tables the flows and their steps are rows of, and what those rows may do. */\nexport interface FlowBinding {\n readonly from: string;\n readonly steps: string;\n /** Columns of the record a `set` step may write. None: the App offers no `set` step. */\n readonly set?: readonly string[];\n /** The events a run may wait for, each with the payload field that names the record it is about. */\n readonly events?: Readonly<Record<string, string>>;\n readonly kinds?: Readonly<Record<string, FlowKind>>;\n}\n\n/** The platform's own step kinds. */\nconst BUILT_IN_KINDS = ['wait', 'until', 'branch', 'set', 'waitFor', 'notify'] as const;\n/** The longest a notify step's words may be: a notice is a line. */\nconst MESSAGE_MOST = 500;\nconst FLOW_TRIGGERS = ['created', 'updated', 'manual'] as const;\nexport type FlowTrigger = (typeof FLOW_TRIGGERS)[number];\n\n/** The columns every flows table has, and every steps table: the rest are read where present. */\nconst FLOW_NEEDED = ['id', 'name', 'start'] as const;\nconst STEP_NEEDED = ['flow_id', 'position', 'kind'] as const;\n/** The most steps one flow runs: a process, not a program. */\nexport const FLOW_STEPS_MOST = 100;\n/** The longest a step may wait: the workflow engine's own ceiling. */\nconst DURATION_MOST = 366 * 86_400_000;\n\nconst IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;\nconst KIND_NAME = /^[A-Za-z][A-Za-z0-9_]{0,39}$/;\nconst EVENT_NAME = /^[A-Za-z][A-Za-z0-9_.-]{0,127}$/;\nconst COMMAND_ID = /^[A-Za-z][A-Za-z0-9_-]*$/;\n\nconst isObject = (value: unknown): value is Record<string, unknown> => !!value && typeof value === 'object' && !Array.isArray(value);\n\n/** `resources.<r>.flows`, read for its shape. Throws the sentence the author reads. */\nexport function parseFlows(raw: unknown, at: string): FlowBinding {\n const here = `${at}, flows`;\n if (!isObject(raw))\n throw new Error(\n `${here} is not an object → { from, steps, set?, events?, kinds? }, like flows: { from: 'po_flow', steps: 'po_flow_step' }.`,\n );\n const unknown = Object.keys(raw).find((key) => !['from', 'steps', 'set', 'events', 'kinds'].includes(key));\n if (unknown) throw new Error(`${here} has \"${unknown}\"; it takes from, steps, set, events and kinds.`);\n for (const name of ['from', 'steps'] as const)\n if (typeof raw[name] !== 'string' || !raw[name].trim())\n throw new Error(`${here}: \"${name}\" names the resource whose rows are the ${name === 'from' ? 'flows' : \"flows' steps\"}.`);\n const binding: { -readonly [K in keyof FlowBinding]: FlowBinding[K] } = {\n from: (raw.from as string).trim(),\n steps: (raw.steps as string).trim(),\n };\n if (raw.set !== undefined) {\n if (!Array.isArray(raw.set) || !raw.set.length || raw.set.some((column) => typeof column !== 'string' || !IDENTIFIER.test(column)))\n throw new Error(`${here}: \"set\" lists the record's own columns a set step may write, like set: ['notes'].`);\n binding.set = Object.freeze([...new Set(raw.set as string[])]);\n }\n if (raw.events !== undefined) {\n if (!isObject(raw.events) || !Object.keys(raw.events).length)\n throw new Error(\n `${here}: \"events\" names each event a run may wait for and the payload field naming the record, like events: { 'po.decided': 'order' }.`,\n );\n for (const [name, field] of Object.entries(raw.events)) {\n if (!EVENT_NAME.test(name))\n throw new Error(`${here}, events \"${name}\": an event is named as defineEvent names it, like 'po.decided'.`);\n if (typeof field !== 'string' || !IDENTIFIER.test(field))\n throw new Error(`${here}, events \"${name}\": name the payload field that holds the record's id, like 'order'.`);\n }\n binding.events = Object.freeze({ ...(raw.events as Record<string, string>) });\n }\n if (raw.kinds !== undefined) {\n if (!isObject(raw.kinds) || !Object.keys(raw.kinds).length)\n throw new Error(`${here}: \"kinds\" names each step kind the App adds, like kinds: { send: { command: 'sendPurchaseOrder' } }.`);\n const kinds: Record<string, FlowKind> = {};\n for (const [name, value] of Object.entries(raw.kinds)) {\n const kind = `${here}, kinds \"${name}\"`;\n if (!KIND_NAME.test(name)) throw new Error(`${kind}: a step kind is a word of letters, digits and underscores.`);\n if ((BUILT_IN_KINDS as readonly string[]).includes(name))\n throw new Error(\n `${kind}: \"${name}\" is one of the platform's own step kinds (${BUILT_IN_KINDS.join(', ')}); name the App's kind another word.`,\n );\n if (!isObject(value)) throw new Error(`${kind} is not an object → { command, input?, waitFor?, remind? }.`);\n const extra = Object.keys(value).find((key) => !['command', 'input', 'waitFor', 'remind'].includes(key));\n if (extra) throw new Error(`${kind} has \"${extra}\"; it takes command, input, waitFor and remind.`);\n if (typeof value.command !== 'string' || !COMMAND_ID.test(value.command))\n throw new Error(`${kind}: \"command\" names one of the App's commands, which the step runs on the record.`);\n const entry: { command: string; input?: readonly string[]; waitFor?: string; remind?: string } = { command: value.command };\n if (value.input !== undefined) {\n if (\n !Array.isArray(value.input) ||\n !value.input.length ||\n value.input.some((column) => typeof column !== 'string' || !IDENTIFIER.test(column.replace(/\\?$/, '')))\n )\n throw new Error(\n `${kind}: \"input\" lists the step columns the command takes, like input: ['approver'] ('note?' for one a step may leave empty).`,\n );\n entry.input = Object.freeze([...(value.input as string[])]);\n }\n if (value.waitFor !== undefined) {\n if (typeof value.waitFor !== 'string' || !binding.events || !Object.hasOwn(binding.events, value.waitFor))\n throw new Error(`${kind}: \"waitFor\" names one of the flows' events, which \"events\" lists.`);\n entry.waitFor = value.waitFor;\n }\n if (value.remind !== undefined) {\n if (typeof value.remind !== 'string' || !COMMAND_ID.test(value.remind) || entry.waitFor === undefined)\n throw new Error(`${kind}: \"remind\" names one of the App's commands, run on the record while a step waits for \"waitFor\".`);\n entry.remind = value.remind;\n }\n kinds[name] = Object.freeze(entry);\n }\n binding.kinds = Object.freeze(kinds);\n }\n return Object.freeze(binding);\n}\n\n/** Every command a binding's kinds run, with the kind that runs it: its step's command, and its reminder. */\nexport const flowKindCommands = (binding: FlowBinding): { kind: string; command: string }[] =>\n Object.entries(binding.kinds ?? {}).flatMap(([kind, declared]) => [\n { kind, command: declared.command },\n ...(declared.remind ? [{ kind, command: declared.remind }] : []),\n ]);\n\n/** A kind's input column, and whether a step may leave it empty. */\nconst kindInputs = (kind: FlowKind): { column: string; optional: boolean }[] =>\n (kind.input ?? []).map((name) => ({ column: name.replace(/\\?$/, ''), optional: name.endsWith('?') }));\n\ninterface CatalogTable {\n readonly name: string;\n readonly columns: readonly { readonly name: string; readonly primaryKey: boolean }[];\n}\n\n/** Why a binding cannot run on this catalog, or undefined. `machine` is the record's state column, which only its verbs move. */\nexport function flowsProblem(\n binding: FlowBinding,\n table: CatalogTable,\n from: CatalogTable | undefined,\n steps: CatalogTable | undefined,\n machine?: string,\n): string | undefined {\n const has = (meta: CatalogTable, name: string) => meta.columns.some((column) => column.name === name);\n if (table.columns.filter((column) => column.primaryKey).length !== 1)\n return `flows: \"${table.name}\" has no single-column key; a flow's run and the events it waits for name a record by one key.`;\n if (!from) return `flows: \"${binding.from}\" is not a declared resource on this connection.`;\n if (!steps) return `flows: \"${binding.steps}\" is not a declared resource on this connection.`;\n const missing = (meta: CatalogTable, needed: readonly string[]) => needed.filter((name) => !has(meta, name));\n const flowGap = missing(from, FLOW_NEEDED);\n if (flowGap.length)\n return `flows: \"${from.name}\" has no ${flowGap.map((name) => `\"${name}\"`).join(', ')}; a flow row has id, name and start (and may have condition, changes and active).`;\n const stepGap = missing(steps, STEP_NEEDED);\n if (stepGap.length)\n return `flows: \"${steps.name}\" has no ${stepGap.map((name) => `\"${name}\"`).join(', ')}; a step row has flow_id, position and kind (and the columns its kind reads).`;\n for (const column of binding.set ?? []) {\n if (!has(table, column)) return `flows: set names \"${column}\", which \"${table.name}\" does not have.`;\n if (table.columns.find((candidate) => candidate.name === column)!.primaryKey) return `flows: set names \"${column}\", the record's key.`;\n if (column === machine) return `flows: set names \"${column}\", the state column of the record's machine, which only its verbs move.`;\n }\n for (const [name, kind] of Object.entries(binding.kinds ?? {}))\n for (const { column } of kindInputs(kind))\n if (!has(steps, column)) return `flows: kind \"${name}\" takes \"${column}\", which \"${steps.name}\" does not have.`;\n return undefined;\n}\n\n/* ---------- a row, judged ---------- */\n\n/** What a row breaks, by the column that says it. */\ntype FlowBreak =\n | 'name'\n | 'start'\n | 'condition'\n | 'changes'\n | 'kind'\n | 'position'\n | 'duration'\n | 'dateField'\n | 'setField'\n | 'goto'\n | 'event'\n | 'input'\n | 'mode'\n | 'remind'\n | 'together'\n | 'audience'\n | 'message'\n | 'flowId';\n\n/** What the server says of a break: a fixed sentence, never the row's value. */\nexport const FLOW_REASONS: Readonly<Record<FlowBreak, string>> = Object.freeze({\n name: 'Name the flow.',\n start: 'Choose created, updated or manual.',\n condition: 'Write a condition in the query language about this record, like status = draft.',\n changes: \"Name one of the record's columns.\",\n kind: 'Choose one of the step kinds this App offers.',\n position: 'Give the step a whole-number position.',\n duration: \"Enter a duration, like '2 hours' or '3 days', at most a year.\",\n dateField: \"Name one of the record's date columns.\",\n setField: 'Name a column a flow may set.',\n goto: \"Name a later step's position.\",\n event: 'Choose one of the events this App’s flows wait for.',\n input: 'This step kind needs a value here.',\n mode: 'Choose any or all: all waits for each one the step asks.',\n remind: \"Enter how often to remind, like '1 day', on a step whose kind reminds.\",\n together: 'A branch decides alone: give it a position no other step has.',\n audience: \"Choose who is told: one of the audiences this App's notices name.\",\n message: `Write what they are told, at most ${MESSAGE_MOST} characters.`,\n flowId: 'Choose the flow this step belongs to.',\n});\n\n/** What the judge knows of the record's table: its columns, its date columns, and whether a condition reads. */\nexport interface FlowRecordShape {\n readonly columns: ReadonlySet<string>;\n readonly dates: ReadonlySet<string>;\n /** Whether a condition is a filter over the record's stored columns. */\n readonly condition: (text: string) => boolean;\n /** The audiences the resource's `notify` rules name `on: { flow }`: whom a `notify` step may tell. */\n readonly audiences?: ReadonlySet<string>;\n}\n\nconst text = (value: unknown): string | undefined => {\n if (value === null || value === undefined) return undefined;\n const said = typeof value === 'string' ? value.trim() : typeof value === 'number' || typeof value === 'boolean' ? String(value) : '';\n return said ? said : undefined;\n};\nconst whole = (value: unknown): number | undefined => {\n const n = typeof value === 'number' ? value : typeof value === 'string' && /^\\s*-?\\d+\\s*$/.test(value) ? Number(value) : NaN;\n return Number.isSafeInteger(n) ? n : undefined;\n};\nconst durationMs = (value: unknown): number | undefined => {\n const ms = parseDuration(text(value));\n return ms && ms <= DURATION_MOST ? ms : undefined;\n};\n\n/** Every break of a flow row, by column. */\nexport function flowRowBreaks(row: Readonly<Record<string, unknown>>, record: FlowRecordShape): { field: string; reason: FlowBreak }[] {\n const breaks: { field: string; reason: FlowBreak }[] = [];\n if (!text(row.name)) breaks.push({ field: 'name', reason: 'name' });\n if (!(FLOW_TRIGGERS as readonly string[]).includes(text(row.start) ?? '')) breaks.push({ field: 'start', reason: 'start' });\n const condition = text(row.condition);\n if (condition !== undefined && !record.condition(condition)) breaks.push({ field: 'condition', reason: 'condition' });\n const changes = text(row.changes);\n if (changes !== undefined && !record.columns.has(changes)) breaks.push({ field: 'changes', reason: 'changes' });\n return breaks;\n}\n\n/** Every break of a step row, by column: an unknown kind, or an input its kind needs and lacks or cannot read. */\nexport function stepRowBreaks(\n row: Readonly<Record<string, unknown>>,\n binding: FlowBinding,\n record: FlowRecordShape,\n): { field: string; reason: FlowBreak }[] {\n const breaks: { field: string; reason: FlowBreak }[] = [];\n const say = (field: string, reason: FlowBreak) => breaks.push({ field, reason });\n if (row.flow_id === null || row.flow_id === undefined || row.flow_id === '') say('flow_id', 'flowId');\n const position = whole(row.position);\n if (position === undefined || position < 0) say('position', 'position');\n const kind = text(row.kind) ?? '';\n const duration = text(row.duration);\n switch (kind) {\n case 'wait':\n if (durationMs(duration) === undefined) say('duration', 'duration');\n break;\n case 'until':\n if (!record.dates.has(text(row.field) ?? '')) say('field', 'dateField');\n if (duration !== undefined && durationMs(duration) === undefined) say('duration', 'duration');\n break;\n case 'branch': {\n const condition = text(row.condition);\n if (condition === undefined || !record.condition(condition)) say('condition', 'condition');\n // A branch only jumps forward, so a run visits each step at most once.\n if (text(row.goto) !== undefined) {\n const to = whole(row.goto);\n if (to === undefined || (position !== undefined && to <= position)) say('goto', 'goto');\n }\n break;\n }\n case 'set':\n if (!(binding.set ?? []).includes(text(row.field) ?? '')) say('field', 'setField');\n break;\n case 'waitFor':\n if (!Object.hasOwn(binding.events ?? {}, text(row.event) ?? '')) say('event', 'event');\n if (duration !== undefined && durationMs(duration) === undefined) say('duration', 'duration');\n break;\n case 'notify': {\n if (!record.audiences?.has(text(row.audience) ?? '')) say('audience', 'audience');\n const message = text(row.message);\n if (message === undefined || message.length > MESSAGE_MOST) say('message', 'message');\n break;\n }\n default: {\n const declared = Object.hasOwn(binding.kinds ?? {}, kind) ? binding.kinds![kind] : undefined;\n if (!declared) {\n say('kind', 'kind');\n break;\n }\n const inputs = kindInputs(declared);\n for (const { column, optional } of inputs) if (!optional && text(row[column]) === undefined) say(column, 'input');\n /* The wait's own columns, where the kind does not take them as input: only a kind that waits has a wait to shape. */\n const own = (column: string) => !inputs.some((input) => input.column === column);\n const waits = declared.waitFor !== undefined;\n const mode = text(row.mode);\n if (own('mode') && mode !== undefined && (!waits || (mode !== 'any' && mode !== 'all'))) say('mode', 'mode');\n const remind = text(row.remind);\n if (own('remind') && remind !== undefined && (!declared.remind || durationMs(remind) === undefined)) say('remind', 'remind');\n if (own('duration') && duration !== undefined && (!waits || durationMs(duration) === undefined)) say('duration', 'duration');\n const condition = text(row.condition);\n if (own('condition') && condition !== undefined && (!waits || !record.condition(condition))) say('condition', 'condition');\n }\n }\n return breaks;\n}\n\n/* ---------- a flow's rows, as the plan a run follows ---------- */\n\n/** One step as a run takes it: read once, when the run starts, and kept with it. */\nexport type PlanStep = Readonly<\n {\n position: number;\n title?: string;\n /** Its place among the steps that share its position, from 1, where more than one does: they start together. */\n member?: number;\n } & (\n | { kind: 'wait'; duration: string }\n | { kind: 'until'; field: string; duration?: string }\n | { kind: 'branch'; condition: string; goto?: number }\n | { kind: 'set'; field: string; value: string | null }\n | { kind: 'waitFor'; event: string; field: string; duration?: string }\n | { kind: 'notify'; audience: string; message: string }\n | {\n kind: 'command';\n name: string;\n command: string;\n input: Readonly<Record<string, string>>;\n waitFor?: { event: string; field: string };\n /** Wait for each one the command asked (its output's `asked`), not the first. */\n mode?: 'all';\n /** The wait ends early once this holds of the record. */\n condition?: string;\n /** The most the wait lasts; then the run goes on. */\n duration?: string;\n /** The command run every `every` while the step waits. */\n remind?: { command: string; every: string };\n }\n )\n>;\n\nexport interface FlowPlan {\n readonly flow: string;\n readonly name: string;\n readonly steps: readonly PlanStep[];\n /** The first row that cannot run: the run stops on it, saying why, before it does anything. */\n readonly problem?: Readonly<{ position: number | null; reason: string }>;\n}\n\n/** A flow's rows as its plan, in position order. Judged as a write judges them, so a row the form refused never runs. */\nexport function flowPlan(\n flow: Readonly<Record<string, unknown>>,\n rows: readonly Readonly<Record<string, unknown>>[],\n binding: FlowBinding,\n record: FlowRecordShape,\n): FlowPlan {\n const id = text(flow.id) ?? '';\n const name = text(flow.name) ?? id;\n /* Steps that share a position keep one order, by their own key, so a run's steps are named the same each time they are read. */\n const ordered = [...rows].sort(\n (a, b) => (whole(a.position) ?? 0) - (whole(b.position) ?? 0) || String(a.id ?? '').localeCompare(String(b.id ?? '')),\n );\n if (ordered.length > FLOW_STEPS_MOST)\n return { flow: id, name, steps: [], problem: { position: null, reason: `A flow runs at most ${FLOW_STEPS_MOST} steps.` } };\n const steps: PlanStep[] = [];\n const shared = new Map<number, number>();\n for (const row of ordered) shared.set(whole(row.position) ?? -1, (shared.get(whole(row.position) ?? -1) ?? 0) + 1);\n const members = new Map<number, number>();\n for (const row of ordered) {\n const position = whole(row.position) ?? -1;\n const broken = stepRowBreaks(row, binding, record)[0];\n if (broken) return { flow: id, name, steps: [], problem: { position, reason: `${broken.field}: ${FLOW_REASONS[broken.reason]}` } };\n const together = shared.get(position)! > 1;\n if (together && (text(row.kind) === 'branch' || text(row.remind) !== undefined))\n return {\n flow: id,\n name,\n steps: [],\n problem: {\n position,\n reason: text(row.kind) === 'branch' ? FLOW_REASONS.together : 'A step that starts together with others sends no reminders.',\n },\n };\n const member = (members.get(position) ?? 0) + 1;\n members.set(position, member);\n const title = text(row.title);\n const head = { position, ...(title ? { title: title.slice(0, 200) } : {}), ...(together ? { member } : {}) };\n const kind = text(row.kind)!;\n const duration = text(row.duration);\n switch (kind) {\n case 'wait':\n steps.push({ ...head, kind, duration: duration! });\n break;\n case 'until':\n steps.push({ ...head, kind, field: text(row.field)!, ...(duration ? { duration } : {}) });\n break;\n case 'branch': {\n const to = whole(row.goto);\n steps.push({ ...head, kind, condition: text(row.condition)!, ...(to === undefined ? {} : { goto: to }) });\n break;\n }\n case 'set':\n steps.push({ ...head, kind, field: text(row.field)!, value: text(row.value) ?? null });\n break;\n case 'waitFor': {\n const event = text(row.event)!;\n steps.push({ ...head, kind, event, field: binding.events![event], ...(duration ? { duration } : {}) });\n break;\n }\n case 'notify':\n steps.push({ ...head, kind, audience: text(row.audience)!, message: text(row.message)! });\n break;\n default: {\n const declared = binding.kinds![kind];\n const inputs = kindInputs(declared);\n const input = Object.fromEntries(\n inputs.flatMap(({ column }) => {\n const value = text(row[column]);\n return value === undefined ? [] : [[column, value]];\n }),\n );\n const own = (column: string) => (inputs.some((each) => each.column === column) ? undefined : text(row[column]));\n const wait = declared.waitFor\n ? {\n waitFor: { event: declared.waitFor, field: binding.events![declared.waitFor] },\n ...(own('mode') === 'all' ? { mode: 'all' as const } : {}),\n ...(own('condition') ? { condition: own('condition')! } : {}),\n ...(own('duration') ? { duration: own('duration')! } : {}),\n ...(declared.remind && own('remind') ? { remind: { command: declared.remind, every: own('remind')! } } : {}),\n }\n : {};\n steps.push({ ...head, kind: 'command', name: kind, command: declared.command, input, ...wait });\n }\n }\n }\n return { flow: id, name, steps };\n}\n\n/** Whether a flow row is switched on: no `active` column, or one that is not false. */\nexport function flowActive(row: Readonly<Record<string, unknown>>): boolean {\n const value = row.active;\n return !(value === false || value === 0 || value === '0' || value === 'false' || value === 'f');\n}\n\n/** Whether a flow starts on this change: its `start`, and an update's `changes` column among the columns that changed.\n * A change naming a flow starts that one alone: a manual start, or `mistri flows start --flow` (flowStarts.ts). */\nexport function flowTriggered(\n row: Readonly<Record<string, unknown>>,\n change: Readonly<{ trigger: FlowTrigger; changed: readonly string[]; flow?: string }>,\n): boolean {\n if (!flowActive(row)) return false;\n const trigger = text(row.start);\n if (change.trigger === 'manual') return trigger === 'manual' && text(row.id) === change.flow;\n if (trigger !== change.trigger || (change.flow !== undefined && text(row.id) !== change.flow)) return false;\n const changes = text(row.changes);\n return change.trigger === 'created' || changes === undefined || change.changed.includes(changes);\n}\n","/**\n * The dashboard sections that are not about one table's figures — a new\n * App's first run, an author's own component with its declared reads, the\n * activity feed across resources — and a section's own time windows.\n * Parsed here, beside the dashboard parser that calls them (appViews.ts).\n */\nimport { ACTIVITY_MAX, FIRST_RUN_MAX_STEPS, SECTION_KEYS, type AppActivityResource, type AppDashSection } from './appModel.js';\nimport { allowKeys, isRecord, optionalText } from '@mistriwork/engine/shared/model.js';\nimport { parseIcon } from '@mistriwork/engine/shared/appIcons.js';\nimport { slug } from '@mistriwork/engine/shared/slug.js';\nimport { WIDTHS, type SectionWidth } from '@mistriwork/engine/shared/page.js';\nimport { checkDeclaredResource, checkResourceSlot, type ParsedData } from './appResourceBlocks.js';\nimport { componentPropValues, parseComponentDecl, parseDashReads, parseDashTotals } from '@mistriwork/engine/shared/appComponents.js';\n\n/** A new App's first steps, each a list view whose New form it opens: drawn until every step's\n table has a row, and alone while none has — next actions, not charts of nothing (Dashboard.tsx). */\n/* The sentence under a heading is `description` everywhere (renamed before 1.0). */\nconst FIRST_RUN_RENAMED = { text: 'is now \"description\": the sentence under the title' } as const;\n/* A destination's words are its `label` (renamed before 1.0). */\nconst FIRST_RUN_STEP_RENAMED = { ...FIRST_RUN_RENAMED, title: 'is now \"label\": the next action' } as const;\n\nexport function firstRunSection(\n s: Record<string, unknown>,\n at: string,\n views: ReadonlyMap<string, { kind: string; key: string }>,\n): AppDashSection {\n allowKeys(s, SECTION_KEYS.firstRun, `${at} (a first run)`, FIRST_RUN_RENAMED);\n const title = optionalText(s, 'title', at);\n const text = optionalText(s, 'description', at);\n const listViews = [...views.values()].filter((view) => view.kind === 'list').map((view) => view.key);\n if (!Array.isArray(s.steps) || !s.steps.length || s.steps.length > FIRST_RUN_MAX_STEPS) {\n throw new Error(\n `${at} is a first run and needs \"steps\": 1 to ${FIRST_RUN_MAX_STEPS} list views to start, like { label: 'Add your first customer', to: 'customers' }.`,\n );\n }\n const steps = s.steps.map((raw, i) => {\n const here = `${at}, step ${i + 1}`;\n if (!isRecord(raw)) throw new Error(`${here} is not an object with \"label\" and \"to\".`);\n allowKeys(raw, ['label', 'description', 'to'], here, FIRST_RUN_STEP_RENAMED);\n const label = optionalText(raw, 'label', here);\n if (!label) throw new Error(`${here} needs a \"label\": the next action, like \"Add your first customer\".`);\n const target = typeof raw.to === 'string' ? views.get(raw.to.trim()) : undefined;\n if (!target || target.kind !== 'list') {\n throw new Error(\n `${here} opens \"${String(raw.to)}\", which is not a list view this App declares. Its list views: ${listViews.join(', ') || 'none'}.`,\n );\n }\n const detail = optionalText(raw, 'description', here);\n return { label, view: target.key, ...(detail ? { detail } : {}) };\n });\n const repeated = steps.find((step, i) => steps.findIndex((other) => other.view === step.view) !== i);\n if (repeated)\n throw new Error(\n `${at} names the view \"${repeated.view}\" in two steps. A step is done when its list has a row, so each view is one step.`,\n );\n return { kind: 'firstRun', ...(title ? { title } : {}), ...(text ? { text } : {}), steps };\n}\n\n/**\n * An author's component on a dashboard: its reads are rows sections in all\n * but drawing — each names its resource, narrows by a query, and may say the\n * `over` the reader's window narrows it by — so the catalog check, the window\n * and the pick filters reach them as they reach every section.\n */\nexport function parseCustomDashSection(s: Record<string, unknown>, at: string, data: ParsedData): AppDashSection {\n const component = parseComponentDecl(s.component, `${at}, \"component\"`, 'section');\n const title = optionalText(s, 'title', at);\n if (s.width !== undefined && !WIDTHS.includes(String(s.width) as SectionWidth)) {\n throw new Error(`${at} has a \"width\" of \"${String(s.width)}\". Use ${WIDTHS.join(', ')}.`);\n }\n const reads = parseDashReads(s.rows, at);\n for (const [name, read] of Object.entries(reads ?? {})) {\n checkResourceSlot(read.table, `${at}, rows \"${name}\", \"from\"`, data);\n checkDeclaredResource(read.table, `${at}, rows \"${name}\", \"from\"`, data);\n }\n const totals = parseDashTotals(s.figures, at);\n for (const [name, total] of Object.entries(totals ?? {})) {\n checkResourceSlot(total.table, `${at}, figures \"${name}\", \"from\"`, data);\n checkDeclaredResource(total.table, `${at}, figures \"${name}\", \"from\"`, data);\n }\n return {\n kind: 'custom',\n component,\n ...(title === undefined ? {} : { title }),\n ...(reads ? { reads } : {}),\n ...(totals ? { aggregates: totals } : {}),\n props: componentPropValues(component, s.props, at),\n ...(s.width === undefined ? {} : { width: s.width as SectionWidth }),\n };\n}\n\n/**\n * A section's own windows: the reader picks this section's period in its\n * heading, apart from the dashboard's window, the first phrase where it\n * starts. They narrow by the section's `over`, as the dashboard's do, so a\n * section without one is refused rather than given a picker that moves\n * nothing. Each phrase is judged against the catalog with the rest of the\n * section (app.ts, resolveDashboard).\n */\nexport function parseSectionWindows(s: Record<string, unknown>, at: string): string[] | undefined {\n if (s.windows === undefined) return undefined;\n if (!Array.isArray(s.windows) || !s.windows.length || s.windows.length > 8 || s.windows.some((w) => typeof w !== 'string' || !w.trim())) {\n throw new Error(`${at} has \"windows\" that are not a list of one to eight phrases like \"last 7 days\".`);\n }\n const windows = (s.windows as string[]).map((w) => w.trim().replace(/\\s+/g, ' '));\n if (new Set(windows.map((w) => w.toLowerCase())).size !== windows.length) throw new Error(`${at} names one of its \"windows\" twice.`);\n if (typeof s.over !== 'string' || !s.over.trim()) {\n throw new Error(`${at} has \"windows\" and no \"over\": name the date column its period narrows, like over: 'moved_at'.`);\n }\n return windows;\n}\n\n/**\n * An embed that works as its room does — its search and filters, and its\n * selection with the bulk actions its room offers the reader — rather than\n * read-only. Opt-in, since most dashboard lists are a glance. The aside draws\n * a list as names, not a table, so there is no room there for its controls,\n * and a board or a calendar is drawn by the dashboard as a board or not at all.\n */\nexport function embedInteractive(\n s: Record<string, unknown>,\n at: string,\n view: string,\n presentation: string | undefined,\n aside: boolean,\n): true | undefined {\n if (s.interactive === undefined || s.interactive === false) return undefined;\n if (s.interactive !== true) throw new Error(`${at} has an \"interactive\" that is not true or false.`);\n if (aside) throw new Error(`${at} embeds \"${view}\" as interactive in the aside, which draws a list as names. Move it into \"sections\".`);\n if (presentation === 'board' || presentation === 'calendar') {\n throw new Error(\n `${at} embeds \"${view}\", a ${presentation}, as interactive; a dashboard draws a ${presentation} read-only. Open its room to work it.`,\n );\n }\n return true;\n}\n\n/**\n * Each section with windows of its own is named in the address by its title\n * (`?period.lines-moved-by-day=last-7-days`), or by its place (`section-3`) where the\n * title has no letters or digits: a name no other windowed\n * section on the dashboard has, and never an empty one.\n */\nexport function nameSectionWindows(sections: AppDashSection[], at: string): void {\n const seen = new Map<string, string>();\n for (const [index, section] of sections.entries()) {\n if (!section.windows) continue;\n const title = section.title ?? '';\n /* A title with no Latin letters or digits (日本語, a reader's words) slugs to nothing: the section's place names it\n instead, so the key is stable in every language and never empty. */\n const key = slug(title, '') || `section-${index + 1}`;\n const other = seen.get(key);\n if (other !== undefined) {\n throw new Error(\n `${at}: \"${other}\" and \"${title}\" both have their own \"windows\" and read as \"${key}\" in the address. Give one another title.`,\n );\n }\n seen.set(key, title);\n section.windowKey = key;\n }\n}\n\n/** How many resources one find box searches: each is a question per keystroke pause. */\nconst FIND_MAX = 6;\n\n/**\n * One box over several resources' records, by their names: each resource\n * named is one the App declares, searched as the reader reads it.\n */\nexport function parseFindSection(s: Record<string, unknown>, at: string, data: ParsedData): AppDashSection {\n const title = optionalText(s, 'title', at);\n const placeholder = optionalText(s, 'placeholder', at);\n if (\n !Array.isArray(s.resources) ||\n !s.resources.length ||\n s.resources.length > FIND_MAX ||\n s.resources.some((name) => typeof name !== 'string')\n ) {\n throw new Error(\n `${at} is a find and needs \"resources\": one to ${FIND_MAX} resource names whose records it searches, like ['article', 'catalog_item'].`,\n );\n }\n const searched = [...new Set(s.resources as string[])];\n for (const name of searched) {\n checkResourceSlot(name, `${at}, resources \"${name}\"`, data);\n checkDeclaredResource(name, `${at}, resources \"${name}\"`, data);\n }\n if (s.width !== undefined && !WIDTHS.includes(String(s.width) as SectionWidth)) {\n throw new Error(`${at} has a \"width\" of \"${String(s.width)}\". Use ${WIDTHS.join(', ')}.`);\n }\n return {\n kind: 'find',\n searched,\n ...(title === undefined ? {} : { title }),\n ...(placeholder === undefined ? {} : { placeholder }),\n ...(s.icon === undefined ? {} : { icon: parseIcon(s.icon, at) }),\n ...(s.width === undefined ? {} : { width: s.width as SectionWidth }),\n };\n}\n\n/**\n * Recent changes across resources, read from their record history: each\n * resource named is one the App declares, carries at most a mark and its word\n * in the feed, and keeps a history from then on (server/recordHistory.ts),\n * as a page's `history()` makes its resource keep one.\n */\nexport function parseActivitySection(s: Record<string, unknown>, at: string, data: ParsedData): AppDashSection {\n const title = optionalText(s, 'title', at);\n if (!isRecord(s.resources) || !Object.keys(s.resources).length || Object.keys(s.resources).length > 12) {\n throw new Error(\n `${at} is an activity and needs \"resources\": one to twelve resources whose changes it lists, like { pick_list: { icon: 'truck' } }.`,\n );\n }\n const resources: Record<string, AppActivityResource> = {};\n for (const [name, raw] of Object.entries(s.resources)) {\n const here = `${at}, resources \"${name}\"`;\n checkResourceSlot(name, here, data);\n checkDeclaredResource(name, here, data);\n if (!isRecord(raw)) throw new Error(`${here} is not an object: { icon?, label? }.`);\n allowKeys(raw, ['icon', 'label'], here);\n const label = optionalText(raw, 'label', here);\n resources[name] = { ...(raw.icon === undefined ? {} : { icon: parseIcon(raw.icon, here) }), ...(label ? { label } : {}) };\n }\n if (s.limit !== undefined && (typeof s.limit !== 'number' || !Number.isInteger(s.limit) || s.limit < 1 || s.limit > ACTIVITY_MAX)) {\n throw new Error(`${at} has a \"limit\" that is not a whole number from 1 to ${ACTIVITY_MAX}: the changes drawn before \"Show more\".`);\n }\n if (s.width !== undefined && !WIDTHS.includes(String(s.width) as SectionWidth)) {\n throw new Error(`${at} has a \"width\" of \"${String(s.width)}\". Use ${WIDTHS.join(', ')}.`);\n }\n return {\n kind: 'activity',\n resources,\n ...(title === undefined ? {} : { title }),\n ...(s.icon === undefined ? {} : { icon: parseIcon(s.icon, at) }),\n ...(s.limit === undefined ? {} : { limit: s.limit as number }),\n ...(s.width === undefined ? {} : { width: s.width as SectionWidth }),\n };\n}\n","/**\n * Content: everything a visitor can open, keyed in `views`, and the `nav`\n * that only arranges references to them. A view is one of:\n *\n * - `list`: the rows of one resource — how they read (`show`, `tabs`,\n * `filters`), how they draw (`presentation`), how they open (`open`);\n * - `dashboard`: sections about whole tables, which may embed a list view;\n * - `form`: one command's form, signed in or public (appForms.ts);\n * - `landing`: the public front page (appLanding.ts).\n *\n * Nav holds a view id, `{ to, label }` for a menu-only word, or a\n * `navGroup` — never an inline definition — and is optional: an App\n * without it has no menu. Every list, dashboard and signed-in form view\n * must be reachable (nav, home, a landing, or a reachable dashboard's embed or\n * note action); a view appears in nav at most once; public views are not in\n * nav, which is signed-in chrome.\n *\n * The App keeps the views it opens as rooms — every view nav references,\n * every list or dashboard a home rule names (the one room of an App without\n * nav), and every list or dashboard a reachable dashboard embeds or names in\n * a note action — under their keys, nav as the tree it was written as, and the home rules as written, for the server\n * to evaluate per visitor. A list view keeps its own words, with its action\n * selector; the New form, the actions and the captions stay the resource's.\n * A dashboard keeps its sections, an embed drawn as the list or board\n * section its view reads as and naming that view, whose room its \"open →\"\n * opens. Every form is kept under its key, public ones too; the landing\n * under its own.\n */\nimport { tabKey } from '@mistriwork/engine/shared/addressParams.js';\nimport { filterAddressWord, listFilterKey } from '@mistriwork/engine/shared/listFilters.js';\nimport { parseViewImport } from './appImport.js';\nimport { LABEL_SEGMENT } from '@mistriwork/engine/shared/recordLabel.js';\nimport { parseCaption } from '@mistriwork/engine/shared/appMessages.js';\nimport { andQuery, splitClauseTail } from '@mistriwork/engine/shared/query.js';\nimport { segmentBookOf, unfoldSegments } from './appSegments.js';\nimport { PREVIOUS_PERIOD } from './dashWindow.js';\nimport { WIDTHS, type SectionWidth } from '@mistriwork/engine/shared/page.js';\nimport { parseTableSizing } from '@mistriwork/engine/shared/tableSizing.js';\nimport { parseListEmpty } from '@mistriwork/engine/shared/listEmpty.js';\nimport { ASSISTANT_ROOM, parseRoutePath } from './appAddress.js';\nimport { destinationProblem, type DestinationKind } from './appDestinations.js';\nimport { parseAppLinkAction } from '@mistriwork/engine/shared/appLinks.js';\nimport { checkGreeting } from './appGreeting.js';\nimport {\n BETTER,\n DASH_CHART_SHAPES,\n DASH_KINDS,\n FILTER_BARS,\n OPEN_MODES,\n SECTION_KEYS,\n VIEW_SECTION_KEYS,\n navViewKeys,\n parseRealm,\n resourceCeiling,\n type AppAuth,\n type AppRoleGrant,\n type AppDashSection,\n type DashTarget,\n type FilterBar,\n type AppDashboardView,\n type AppFormView,\n type AppHomeRule,\n type AppLanding,\n type AppListView,\n type AppNavNode,\n DEFAULT_WINDOWS,\n} from './appModel.js';\nimport {\n FIGURE_FORMATS,\n FIGURE_FORMAT_WORDS,\n DISPLAY_TONES,\n VIEW_KEY,\n allowKeys,\n humanName,\n isRecord,\n keyHomesOf,\n optionalText,\n} from '@mistriwork/engine/shared/model.js';\nimport {\n CALENDAR_BACKDROPS_MAX,\n type AppBoardFact,\n type AppCalendarBackdrop,\n type AppListPresentation,\n type AppListTab,\n type AppSplitListDetail,\n} from '@mistriwork/engine/shared/listPresentations.js';\nimport { parseEditable } from './appListEditable.js';\nimport { parseFormView } from './appForms.js';\nimport { headingOf } from '@mistriwork/engine/shared/appNoun.js';\nimport { landingViews, parseLandingView } from './appLanding.js';\nimport { CARDS_MAX_LINES, parseCards } from '@mistriwork/engine/shared/appCards.js';\nimport { checkDeclaredResource, checkResourceSlot, type ParsedData } from './appResourceBlocks.js';\nimport { parseHome, type HomeView } from './appHome.js';\nimport { parseIcon } from '@mistriwork/engine/shared/appIcons.js';\nimport { unknownKey } from '@mistriwork/engine/shared/suggest.js';\nimport { isAreaName } from '@mistriwork/engine/shared/dashLayout.js';\nimport {\n embedInteractive,\n firstRunSection,\n nameSectionWindows,\n parseActivitySection,\n parseCustomDashSection,\n parseFindSection,\n parseSectionWindows,\n} from './appDashSections.js';\nimport { parseCustomList, parsePage } from '@mistriwork/engine/shared/appComponents.js';\nimport { conditionsOnly, parseChartShape, parseAreas, parseFunnelStages, parseNoteBanner, parseStepsShape } from './appDashShapes.js';\nimport { AppProblems, gather } from './appProblems.js';\nimport { autoKeyOf } from '@mistriwork/engine/shared/autoBucket.js';\nimport { parseOpenRoles } from './appViewRoles.js';\nimport { parseNav } from './appNav.js';\nimport { isRegisteredCommand, registeredCommand } from './command.js';\n\n/* Each view as parsed: the AppDef view itself, beside what only parsing\n needs — its key and where it was written, and a dashboard's tables and\n embeds, which the home and reachability checks read. */\ninterface ListView {\n kind: 'list';\n key: string;\n at: string;\n view: AppListView;\n}\ninterface DashboardView {\n kind: 'dashboard';\n key: string;\n at: string;\n view: AppDashboardView;\n /** Every table a section reads, embeds included: what a home rule's roles must read. */\n tables: string[];\n embeds: string[];\n}\ninterface LandingView {\n kind: 'landing';\n key: string;\n at: string;\n raw: Record<string, unknown>;\n}\ninterface FormView {\n kind: 'form';\n key: string;\n at: string;\n form: AppFormView;\n}\ntype ParsedView = ListView | DashboardView | LandingView | FormView;\n\n/** A parsed view's kind, as a destination reads it (appDestinations.ts). */\nconst parsedKind = (view: ParsedView | undefined): DestinationKind | undefined =>\n view === undefined ? undefined : view.kind === 'form' ? (view.form.public ? 'public form' : 'form') : view.kind;\n\ninterface ContentContext {\n data: ParsedData;\n name: string;\n auth?: AppAuth;\n api?: import('./appApi.js').AppApiDef;\n}\ninterface ParsedContent {\n views: Record<string, AppListView | AppDashboardView | AppFormView>;\n nav: AppNavNode[];\n home: AppHomeRule[];\n landing?: { view: string; content: AppLanding };\n}\n\n/** A view's Favorites star is opt-in: true, or false, which is the same as leaving it out. */\nfunction favoriteFlag(value: unknown, at: string): boolean {\n if (value === undefined) return false;\n if (typeof value !== 'boolean') throw new Error(`${at}: \"favorite\" must be true (a star on the view) or false.`);\n return value;\n}\n\n/* Spellings renamed before 1.0, refused with the word that replaced them (allowKeys' `refused`). */\nconst LIST_VIEW_RENAMED = {\n table: 'is now \"from\": the resource whose rows the list shows',\n keys: 'is now \"shortcuts\": a split\\'s single keys for the picked record\\'s actions',\n} as const;\nconst BACKDROP_RENAMED = {\n resource: 'is now \"from\", and the date columns a span runs between are \"start\" and \"end\"',\n to: 'is now \"end\"',\n label: 'is now \"labelColumn\": the column whose value names a span',\n} as const;\n/* Captioned values are `properties` (a fields section's presentation of that name); `facts` is the dotted line only. */\nconst PRESENTATION_RENAMED = {\n queue: {\n facts: 'is now \"properties\": the captioned values under the title (a dotted line is a card\\'s \"facts\")',\n },\n cards: {\n lines: 'is now \"properties\": the captioned values under the title (\"facts\" stays the dotted line)',\n },\n} as const;\nconst DASHBOARD_VIEW_RENAMED = {\n table: 'is now \"from\": the resource every section that names none reads',\n layout: 'is now \"areas\": the rows of area names its sections stand in (\"layout\" is a page\\'s archetype)',\n refresh: 'is gone: a dashboard follows changes as they happen and re-reads what depends on the time each minute',\n} as const;\nexport const LIST_VIEW_KEYS = [\n 'kind',\n 'from',\n 'label',\n 'path',\n 'icon',\n 'query',\n // Read before the view is (appSegments.ts unfoldSegments): the segment's word joins `query`.\n 'segment',\n 'show',\n 'presentation',\n 'board',\n 'split',\n 'empty',\n 'tabs',\n 'filters',\n 'filterBar',\n 'suggest',\n 'advancedFilters',\n 'quickAdd',\n 'favorite',\n 'totals',\n 'density',\n 'rowActions',\n 'editable',\n 'open',\n 'opens',\n 'sizing',\n 'actions',\n 'sort',\n 'order',\n 'import',\n 'shortcuts',\n 'heading',\n 'roles',\n 'count',\n] as const;\nexport const DASHBOARD_VIEW_KEYS = [\n 'kind',\n 'label',\n 'path',\n 'heading',\n 'description',\n 'icon',\n 'from',\n 'sections',\n 'aside',\n 'areas',\n 'windows',\n 'window',\n 'windowControl',\n 'filters',\n 'favorite',\n 'roles',\n] as const;\nexport const PRESENTATION_KEYS = {\n checklist: ['kind', 'primary', 'checked', 'lines'],\n queue: ['kind', 'primary', 'lines', 'properties', 'badge', 'openLabel'],\n board: ['kind', 'by', 'lanes', 'order', 'swimlanes', 'limits', 'primary', 'lines', 'category', 'footer', 'sum', 'folded'],\n calendar: ['kind', 'date', 'end', 'primary', 'lines', 'badge', 'backdrop'],\n tree: ['kind', 'parent', 'primary', 'lines'],\n gantt: ['kind', 'start', 'end', 'primary', 'by', 'progress'],\n cards: ['kind', 'primary', 'description', 'value', 'badge', 'icon', 'properties', 'facts', 'footer', 'image', 'media'],\n split: ['kind', 'detail', 'limit', 'row'],\n} as const;\n/** How many `lines` a card below its primary may carry, by presentation. */\nconst MAX_LINES = {\n checklist: 1,\n queue: 2,\n board: 1,\n calendar: 1,\n tree: 1,\n cards: CARDS_MAX_LINES,\n} as const;\n/* A sort as the query bar writes one: columns or label paths, each with an optional direction. */\nconst SORT_TEXT = /^\\s*[A-Za-z_][\\w.]*(\\s+(asc|desc))?(\\s*,\\s*[A-Za-z_][\\w.]*(\\s+(asc|desc))?)*\\s*$/i;\n\nexport function parseContent(a: Record<string, unknown>, where: string, ctx: ContentContext): ParsedContent {\n if (a.views !== undefined && !isRecord(a.views)) throw new Error(`${where}: \"views\" must be an object of views keyed by id.`);\n const entries = Object.entries((a.views ?? {}) as Record<string, unknown>);\n const views = new Map<string, ParsedView>();\n /* Two passes: a dashboard's embeds read the list views, so every other\n view is parsed first. */\n /* Each view is judged on its own, so one run names every view's refusal\n (appProblems.ts); the checks across views wait until all of them parse. */\n const problems: string[] = [];\n const failed = new Set<string>();\n const book = segmentBookOf(ctx.data.resources);\n const unfolded = new Map<string, Record<string, unknown>>();\n for (const [key, authored] of entries) {\n const at = `${where}: views \"${key}\"`;\n const parsed = gather(problems, () => {\n if (!VIEW_KEY.test(key)) throw new Error(`${at}: a view key is its address, and must be lowercase letters, digits and hyphens.`);\n if (!isRecord(authored)) throw new Error(`${at} is not an object.`);\n // A segment named anywhere in the view is its word in a query from here on (appSegments.ts).\n const value = unfoldSegments(authored, book, at) as Record<string, unknown>;\n unfolded.set(key, value);\n const kind = viewKind(value, at);\n if (kind === 'list')\n views.set(key, {\n kind,\n key,\n at,\n view: withOpenRoles(parseListView(value, at, ctx.data), value, at, ctx),\n });\n else if (kind === 'form')\n views.set(key, {\n kind,\n key,\n at,\n form: withOpenRoles(parseFormView(value, at), value, at, ctx),\n });\n else if (kind === 'landing') views.set(key, { kind, key, at, raw: value });\n return true;\n });\n if (!parsed) failed.add(key);\n }\n for (const [key, value] of entries) {\n if (failed.has(key)) continue;\n const said: string[] = [];\n gather(said, () => {\n const at = `${where}: views \"${key}\"`;\n const raw = unfolded.get(key) ?? (value as Record<string, unknown>);\n const parsed =\n raw.kind === 'dashboard'\n ? parseDashboardView(raw, key, at, ctx.data, views, Boolean(ctx.auth))\n : raw.kind === 'page'\n ? parsePageView(raw, key, at, ctx)\n : undefined;\n if (parsed)\n views.set(key, {\n ...parsed,\n view: withOpenRoles(parsed.view, raw, at, ctx),\n });\n });\n /* A dashboard over a list that did not parse would only restate that list's refusal. */\n problems.push(...said.filter((problem) => ![...failed].some((bad) => problem.includes(`\"${bad}\"`))));\n }\n if (problems.length) throw new AppProblems(problems);\n /* The checks across views are independent of each other too: each is gathered, so the menu's typo and the home's\n are said in one run, and only the reachability walk, which reads both, waits until they parse. */\n const across: string[] = [];\n const order = entries.map(([key]) => key);\n const apiViews = ctx.api?.views.map((entry) => entry.view) ?? [];\n for (const key of apiViews) {\n if (views.get(key)?.kind !== 'list') across.push(`${where}: api.views.${key} must reference an existing list view.`);\n }\n const ofKind = <K extends ParsedView['kind']>(kind: K) =>\n order.map((key) => views.get(key)!).filter((view): view is Extract<ParsedView, { kind: K }> => view.kind === kind);\n\n const forms = ofKind('form');\n if (forms.length && !ctx.auth?.roles) {\n across.push(`${where}: form views require \"auth\" and \"auth.roles\". A form runs a command, and commands need both.`);\n } else gather(across, () => checkForms(forms, ctx));\n for (const source of ofKind('list')) {\n const said = [\n { at: `${source.at}, \"empty.action.view\"`, action: source.view.empty?.action },\n ...(source.view.tabs ?? []).map((tab, i) => ({ at: `${source.at}, tab ${i + 1}, \"empty.action.view\"`, action: tab.empty?.action })),\n ];\n for (const { at, action } of said) {\n if (!action) continue;\n /* A room, by the one rule every action's destination answers to (appDestinations.ts): \"No canned responses:\n write one\" opens the signed-in form that writes one. */\n const problem = destinationProblem(at, action.view, parsedKind(views.get(action.view)));\n if (problem) across.push(problem);\n }\n }\n /* A dashboard's note, a banner too, opens a room by the same rule. Its target becomes a room only when this\n dashboard is reached from nav, home, a landing or a reachable dashboard; final payload projection still\n removes lost targets. */\n for (const source of ofKind('dashboard')) {\n const placed = [\n ...source.view.sections.map((section, i) => ({ section, at: `${source.at}, section ${i + 1}` })),\n ...(source.view.aside ?? []).map((section, i) => ({ section, at: `${source.at}, \"aside\", section ${i + 1}` })),\n ];\n for (const { section, at } of placed) {\n if (section.kind !== 'note' || !section.action || 'to' in section.action) continue;\n const problem = destinationProblem(`${at}, note action`, section.action.view, parsedKind(views.get(section.action.view)));\n if (problem) across.push(problem);\n }\n }\n const publicForms = new Set(forms.filter((view) => view.form.public).map((view) => view.key));\n /* What a landing may open and show: the public forms, and — with\n `auth.public` — every list and dashboard whose tables the public reads\n all of, as a signed-out home may name (appHome.ts). */\n const publicReads = new Set(ctx.auth?.public?.read ?? []);\n const openLists = new Map(\n ofKind('list')\n .filter((view) => publicReads.has(view.view.table))\n .map((view) => [view.key, view.view]),\n );\n const openViews = new Set([\n ...openLists.keys(),\n ...ofKind('dashboard')\n .filter((view) => view.tables.length && view.tables.every((table) => publicReads.has(table)))\n .map((view) => view.key),\n ]);\n const images = new Set(\n Object.entries(ctx.data.resources ?? {})\n .filter(([name, resource]) => resource.file && publicReads.has(name))\n .map(([name]) => name),\n );\n const landingsSaid = across.length;\n const landings = ofKind('landing').flatMap((view) => {\n const landing = gather(across, () =>\n parseLandingView(view.raw, view.at, {\n forms: publicForms,\n views: openViews,\n lists: openLists,\n images,\n }),\n );\n return landing ? [{ key: view.key, landing }] : [];\n });\n /* A landing that did not parse names nothing: what it would have opened is not known, so nothing is judged unreached. */\n const landingsRead = across.length === landingsSaid;\n const landingKeys = landings.flatMap(({ landing }) => landingViews(landing));\n /* A landing is the signed-out front page: what it shows or opens is for visitors who hold no role. */\n for (const key of landingKeys) {\n const view = views.get(key);\n const roles =\n view?.kind === 'form' ? view.form.roles : view?.kind === 'list' || view?.kind === 'dashboard' ? view.view.roles : undefined;\n if (roles)\n across.push(\n `${view!.at} names \"roles\", and the landing opens it to signed-out visitors, who hold none. Remove \"roles\", or the landing's link to it.`,\n );\n }\n if (landings.length > 1)\n across.push(`${where}: views \"${landings[0].key}\" and \"${landings[1].key}\" are both landings. An App has one front page.`);\n if (landings.length && !ctx.auth) {\n across.push(\n `${where}: a landing view requires \"auth\". An App without sign-in has no signed-out front door — every visitor already sees the App.`,\n );\n }\n\n /* An assistant with a page of its own is a room nav may place, by its word, as a view is (appAssistant.ts). */\n const assistantPage =\n isRecord(a.assistant) && Array.isArray(a.assistant.placement) && (a.assistant.placement as unknown[]).includes('page');\n if (assistantPage && views.has(ASSISTANT_ROOM))\n across.push(\n `${where}: views \"${ASSISTANT_ROOM}\" takes the word the assistant's page is placed by (assistant placement 'page'). Give the view another key.`,\n );\n const nav = gather(across, () => parseNav(a.nav, where, views, order, parsedKind, assistantPage));\n const homeViews = new Map<string, HomeView>(\n order.map((key) => {\n const view = views.get(key)!;\n if (view.kind === 'form') {\n return [\n key,\n {\n kind: 'form',\n tables: [],\n command: view.form.command.id,\n ...(view.form.public ? { public: true as const } : {}),\n ...(view.form.roles ? { roles: view.form.roles } : {}),\n },\n ];\n }\n const roles = view.kind === 'landing' ? undefined : view.view.roles;\n return [\n key,\n {\n kind: view.kind,\n tables: view.kind === 'list' ? [view.view.table] : view.kind === 'dashboard' ? view.tables : [],\n ...(roles ? { roles } : {}),\n },\n ];\n }),\n );\n const rules = gather(across, () => parseHome(a.home, where, homeViews, ctx.auth, resourceCeiling(ctx.data).read, ctx.name));\n /* What nothing opens is judged only once the menu, the home and the landing are all known: one that did not parse\n would make every room it names look orphaned. */\n if (!nav || !rules || !landingsRead) throw new AppProblems(across);\n // The assistant's room is nav's to place and no view's: nothing below serves it as one.\n const navKeys = navViewKeys(nav).filter((key) => views.has(key));\n\n /* A list, dashboard or signed-in form view nothing can open is a typo or\n an orphan; either way it would go unserved without a word. A public\n form is opened at its own address. Dashboard embeds, the lists metrics\n open and note actions are\n traversed only after a root reaches their dashboard: two orphan notes\n pointing at each other never manufacture a room. */\n /* A list a record's page embeds is opened by that page: a thread's posts, drawn only on the thread. */\n const pageEmbeds = Object.values(isRecord(a.pages) ? a.pages : {}).flatMap((page) =>\n (isRecord(page) && Array.isArray(page.sections) ? page.sections : []).flatMap((section) =>\n isRecord(section) && section.kind === 'view' && typeof section.view === 'string' ? [section.view] : [],\n ),\n );\n /* The header's New menu opens its views and the forms of its commands (ui.shell.new; app.ts checks the names). */\n const rawNew = (isRecord(a.ui) && isRecord(a.ui.shell) && Array.isArray(a.ui.shell.new) ? a.ui.shell.new.filter(isRecord) : []).flatMap(\n (entry) => (Array.isArray(entry.children) ? entry.children.filter(isRecord) : [entry]),\n );\n const newKeys = [\n ...rawNew.flatMap((entry) => (typeof entry.view === 'string' && views.has(entry.view) ? [entry.view] : [])),\n ...forms.filter(({ form }) => !form.public && rawNew.some((entry) => entry.command === form.command.id)).map(({ key }) => key),\n ];\n const reachable = new Set([...navKeys, ...rules.map((rule) => rule.view), ...landingKeys, ...apiViews, ...pageEmbeds, ...newKeys]);\n for (const key of reachable) {\n const view = views.get(key);\n if (view?.kind !== 'dashboard') continue;\n for (const target of [\n ...view.embeds,\n ...[...view.view.sections, ...(view.view.aside ?? [])].flatMap((section) =>\n section.kind === 'note' && section.action && 'view' in section.action\n ? [section.action.view]\n : section.kind === 'metric' && section.view\n ? [section.view]\n : section.kind === 'firstRun'\n ? (section.steps ?? []).map((step) => step.view)\n : [],\n ),\n ])\n reachable.add(target);\n }\n for (const key of order) {\n const view = views.get(key)!;\n if (view.kind === 'form' && !view.form.public && !reachable.has(key)) {\n across.push(\n `${view.at} is a signed-in form referenced by nothing — not nav, not home, not the New menu (ui.shell.new) — so nothing could open it. Reference it, or remove it.`,\n );\n }\n if (view.kind === 'dashboard' && !reachable.has(key)) {\n across.push(\n `${view.at} is referenced by nothing — not nav, not home, not a reachable dashboard's embed, metric or note action, or a landing's { kind: 'view' } section — so nothing could open it. Reference it, or remove it.`,\n );\n }\n }\n if (across.length) throw new AppProblems(across);\n /* A list nothing links to is still a list: a view is everything a visitor\n can open, and `nav` arranges the menu over some of them. An unlinked one\n is served at its address and offered by ⌘K, and no menu lists it — the\n lookup lists an ops App keeps (a code table, a log) without crowding the\n side menu. A dashboard stays refused: what it opens is only reached\n through it, and an orphan pair must not manufacture rooms. */\n for (const key of order) if (views.get(key)!.kind === 'list') reachable.add(key);\n /* So is a free-form page: served at its address, which another page or a component may link to with nav: false. */\n for (const key of order) {\n const view = views.get(key)!;\n if (view.kind === 'dashboard' && view.view.page) reachable.add(key);\n }\n\n /* The served set includes every ordinary root and every room a reachable\n dashboard opens. Navigation still decides the sidebar; an action target\n is a room only after its source dashboard is independently reachable. */\n const served: Record<string, AppListView | AppDashboardView | AppFormView> = {};\n for (const key of reachable) {\n const view = views.get(key)!;\n if (view.kind === 'landing' || (view.kind === 'form' && view.form.public)) continue;\n served[key] = view.kind === 'form' ? view.form : view.view;\n }\n /* Then the public forms, which no menu names: each is served at its own address. */\n for (const view of forms) if (view.form.public) served[view.key] = view.form;\n const [landing] = landings;\n return {\n views: served,\n nav,\n home: rules.map(({ at: _at, ...rule }) => rule),\n ...(landing ? { landing: { view: landing.key, content: landing.landing } } : {}),\n };\n}\n\n/*\n * A form's command is granted like any other, and for a signed-in form the\n * grant is its whole audience: one whose command no role holds could be\n * opened by nobody. A command a form runs creates its rows and names none,\n * so no action runs it on a record too.\n */\nfunction checkForms(forms: readonly FormView[], ctx: ContentContext): void {\n const grants = Object.values(ctx.auth?.roles ?? {});\n const actions = Object.entries(ctx.data.resources ?? {}).flatMap(([name, resource]) =>\n (resource.actions ?? []).flatMap((action) => (action.command ? [{ name, id: action.command.id }] : [])),\n );\n for (const { at, form } of forms) {\n const id = form.command.id;\n const acting = actions.find((action) => action.id === id);\n if (acting) {\n throw new Error(\n `${at} runs the command \"${id}\", which an action on resources \"${acting.name}\" also runs. ` +\n \"A form's command creates records and names none; an action's names the record it acts on. Register a command for each.\",\n );\n }\n if (form.public) continue;\n /* Under \"auth.rows\" a signed-in form is served too: its command runs in\n the member's scope, so a row it inserts must land inside it (the scoped\n transaction refuses one that does not), and a tenant's owner column is\n the runtime's to fill, never the handler's. */\n if (!grants.some((grant) => grant.commands?.includes(id))) {\n throw new Error(\n `${at} runs the command \"${id}\", which no role in auth.roles is granted, so nobody could open the form. Grant it in auth.roles.<role>.commands.`,\n );\n }\n }\n}\n\nfunction viewKind(value: Record<string, unknown>, at: string): ParsedView['kind'] {\n const kind = value.kind;\n if (kind === undefined) {\n /* A view without its kind: say which one its keys suggest. */\n if (Object.hasOwn(value, 'sections')) throw new Error(`${at} has no \"kind\"; a dashboard view says kind: 'dashboard'.`);\n if (Object.hasOwn(value, 'from')) throw new Error(`${at} has no \"kind\"; a list view says kind: 'list'.`);\n throw new Error(\n `${at} has no \"kind\". A view is one of: list, dashboard, form or landing, written with listView(), dashboard(), commandForm() or landing(); a free-form page is a pages/*.page.tsx file.`,\n );\n }\n if (kind !== 'list' && kind !== 'dashboard' && kind !== 'landing' && kind !== 'form' && kind !== 'page') {\n throw new Error(\n `${at} has a \"kind\" of \"${String(kind)}\". A view is one of: list, dashboard, form or landing, written with listView(), dashboard(), commandForm() or landing(); a free-form page is a pages/*.page.tsx file.`,\n );\n }\n /* A free-form page is served as the dashboard that draws it (parsePageView). */\n return kind === 'page' ? 'dashboard' : kind;\n}\n\n/* ---------- list views ---------- */\n\n/* A board or a split is a key of the list view that carries its options, so the view says which it is by the key and\n the two cannot be written together; the rest draw through `presentation`, written with their helpers. Both parse to\n the one `presentation` the rest of the platform reads. */\nfunction listPresentation(v: Record<string, unknown>, at: string): AppListPresentation | undefined {\n const keyed = (['board', 'split'] as const).filter((key) => v[key] !== undefined);\n const kind = isRecord(v.presentation) ? v.presentation.kind : undefined;\n if (kind === 'board' || kind === 'split') {\n throw new Error(\n `${at} has a \"presentation\" of kind \"${kind}\". A ${kind} is a key of the list view: ${kind}: { ${kind === 'board' ? \"by: 'status', primary: 'name'\" : \"detail: ['name']\"} }.`,\n );\n }\n if (keyed.length + (v.presentation === undefined ? 0 : 1) > 1) {\n throw new Error(\n `${at} has ${[...keyed.map((key) => `\"${key}\"`), ...(v.presentation === undefined ? [] : ['\"presentation\"'])].join(' and ')}. A list draws its rows one way: a board, a split or a presentation.`,\n );\n }\n if (keyed.length === 0) return v.presentation === undefined ? undefined : parsePresentation(v.presentation, at);\n const key = keyed[0];\n if (!isRecord(v[key])) throw new Error(`${at}: \"${key}\" must be an object${key === 'board' ? ' with at least \"by\"' : ''}.`);\n allowKeys(\n v[key] as Record<string, unknown>,\n PRESENTATION_KEYS[key].filter((name) => name !== 'kind'),\n `${at}, \"${key}\"`,\n );\n return parsePresentation({ ...(v[key] as object), kind: key }, at);\n}\n\n/* A text filter's suggestions: the distinct values of a few of the list's own columns, offered as the reader types.\n Keyed by the filter they fill, which the list must declare: a suggestion for a box nobody draws is a typo. */\nfunction parseSuggest(raw: unknown, filters: string[] | undefined, at: string): Record<string, string[]> {\n const where = `${at}, \"suggest\"`;\n if (!isRecord(raw) || !Object.keys(raw).length)\n throw new Error(\n `${where} must name a text filter and the columns whose values it suggests, like suggest: { title: ['make', 'model'] }.`,\n );\n const declared = (filters ?? []).map((name) => name.trim());\n return Object.fromEntries(\n Object.entries(raw).map(([filter, columns]) => {\n if (!declared.includes(filter))\n throw new Error(`${where} names \"${filter}\", which is not one of this list's \"filters\". Suggestions fill a filter the list draws.`);\n if (\n !Array.isArray(columns) ||\n !columns.length ||\n columns.some((column) => typeof column !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(column.trim()))\n )\n throw new Error(`${where}, \"${filter}\" must be a list of the resource's own column names, like ['make', 'model'].`);\n return [filter, [...new Set((columns as string[]).map((column) => column.trim()))]];\n }),\n );\n}\n\nfunction parseListView(v: Record<string, unknown>, at: string, data: ParsedData): AppListView {\n allowKeys(v, [...LIST_VIEW_KEYS, 'realm'], at, LIST_VIEW_RENAMED);\n if (typeof v.from !== 'string' || !v.from.trim()) throw new Error(`${at} has no \"from\": a list view shows the rows of one resource.`);\n const table = v.from.trim();\n checkResourceSlot(table, `${at}, \"from\"`, data);\n checkDeclaredResource(table, `${at}, \"from\"`, data);\n const resource = data.resources![table];\n if (v.advancedFilters !== undefined && typeof v.advancedFilters !== 'boolean') {\n throw new Error(`${at}: \"advancedFilters\" must be true or false.`);\n }\n if (typeof v.filterBar === 'boolean')\n throw new Error(`${at}: \"filterBar\" is now one of 'bar', 'rail' or 'none': ${v.filterBar ? \"true is 'bar'\" : \"false is 'none'\"}.`);\n if (v.filterBar !== undefined && !FILTER_BARS.includes(v.filterBar as FilterBar)) {\n throw new Error(`${at}: \"filterBar\" must be 'bar', 'rail' (the filters as a rail of facets beside the rows) or 'none'.`);\n }\n if (v.quickAdd !== undefined && typeof v.quickAdd !== 'boolean') {\n throw new Error(`${at}: \"quickAdd\" must be true (inline resources.${table}.form, or the schema's New form) or false.`);\n }\n if (v.open === 'peek') throw new Error(`${at} has an \"open\" of \"peek\", which is now \"drawer\", as the New form's is.`);\n if (v.open !== undefined && !(OPEN_MODES as readonly unknown[]).includes(v.open)) {\n throw new Error(`${at} has an \"open\" of \"${String(v.open)}\". Use \"page\", \"drawer\" or \"modal\".`);\n }\n if (v.filters !== undefined && !Array.isArray(v.filters)) throw new Error(`${at} has \"filters\" that are not a list of column names.`);\n /* A filter across a reference or over the rows that point at the record is held as the words its condition\n starts with (listFilters.ts), so everything downstream reads one kind of filter. */\n const filters = (v.filters as unknown[] | undefined)?.map((filter) => listFilterKey(filter, at));\n const words = new Map<string, string>();\n for (const filter of filters ?? []) {\n const word = filterAddressWord(filter);\n if (words.has(word)) throw new Error(`${at} names the filter \"${words.get(word)}\" twice.`);\n words.set(word, filter);\n }\n const suggest = v.suggest === undefined ? undefined : parseSuggest(v.suggest, filters, at);\n const importing = v.import === undefined ? undefined : parseViewImport(v.import, at);\n if (importing && !importing.command && resource.access !== 'write') {\n throw new Error(\n `${at} has \"import\" over resources \"${table}\", which the App does not write; importing creates records, which needs access 'write'.`,\n );\n }\n const presentation = listPresentation(v, at);\n if (presentation?.kind === 'calendar')\n for (const [i, span] of (presentation.backdrop ?? []).entries()) {\n const here = `${at}, presentation, backdrop${(presentation.backdrop?.length ?? 0) > 1 ? ` ${i + 1}` : ''}, \"from\"`;\n checkResourceSlot(span.from, here, data);\n checkDeclaredResource(span.from, here, data);\n }\n const empty = v.empty === undefined ? undefined : parseListEmpty(v.empty, at);\n if (empty && (presentation?.kind === 'calendar' || presentation?.kind === 'tree' || presentation?.kind === 'gantt')) {\n throw new Error(\n `${at} has \"empty\" and a ${presentation.kind} presentation. A ${presentation.kind} reads its own rows, so use its ordinary empty state.`,\n );\n }\n /* A split's list is the narrow pane beside a record; a rail beside it would leave the rows no room. */\n if (v.filterBar === 'rail' && presentation?.kind === 'split') {\n throw new Error(\n `${at} has filterBar 'rail' and a split presentation. A rail sits beside a full list; a split's list keeps the bar (or filterBar: 'none').`,\n );\n }\n /* A board over a machine's column reads its lanes from the machine (app.ts withMachineLanes): one order, said once. */\n if (presentation?.kind === 'board' && presentation.lanes !== undefined && resource?.machine?.column === presentation.by) {\n throw new Error(\n `${at}: presentation \"lanes\" orders \"${presentation.by}\", which the machine of resources.${table} moves; ` +\n 'its lanes are the machine\\'s states in its order. Drop \"lanes\".',\n );\n }\n /* A card's place is the reader's to change by hand; a column the resource moves only through its actions never is. */\n if (presentation?.kind === 'board' && presentation.order !== undefined && resource?.machine?.column === presentation.order) {\n throw new Error(\n `${at}: presentation \"order\" names \"${presentation.order}\", which the machine of resources.${table} moves. ` +\n \"A card's place is the reader's to change; name another numeric column.\",\n );\n }\n let totals: string[] | undefined;\n if (v.totals !== undefined) {\n if (\n !Array.isArray(v.totals) ||\n !v.totals.length ||\n v.totals.some((c) => typeof c !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(c))\n ) {\n throw new Error(`${at} has \"totals\" that are not a list of the resource's own numeric columns.`);\n }\n if (new Set(v.totals).size !== v.totals.length) throw new Error(`${at} has \"totals\" that name one column twice.`);\n if (presentation) throw new Error(`${at} has \"totals\" and a ${presentation.kind} presentation; a footer of totals belongs to a grid.`);\n totals = v.totals as string[];\n }\n if (v.rowActions !== undefined) {\n if (typeof v.rowActions !== 'boolean') throw new Error(`${at}: \"rowActions\" must be true (a ⋯ menu on each row) or false.`);\n if (v.rowActions && presentation)\n throw new Error(`${at} has \"rowActions\" and a ${presentation.kind} presentation; a row menu belongs to a grid.`);\n }\n const editable = parseEditable(v, at, table, resource, presentation);\n if (v.density !== undefined && v.density !== 'comfortable' && v.density !== 'compact') {\n throw new Error(`${at} has a \"density\" of \"${String(v.density)}\". Use \"comfortable\" or \"compact\".`);\n }\n /* Row height and column sizing are a grid's. A split's list is drawn as\n one; the card presentations draw no grid for either to reach. */\n for (const key of ['density', 'sizing'] as const) {\n if (v[key] !== undefined && presentation && presentation.kind !== 'split') {\n throw new Error(\n `${at} has \"${key}\" and a ${presentation.kind} presentation; ${key === 'density' ? 'a row height' : 'column sizing'} belongs to a grid.`,\n );\n }\n }\n /* A join table's list opens the record each row is about. The grid's rows only: a card, a lane or a\n split's pick is drawn and acted on as the row it is. */\n if (v.opens !== undefined) {\n if (typeof v.opens !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(v.opens.trim())) {\n throw new Error(`${at}: \"opens\" must name one of the resource's reference columns, whose record a row opens.`);\n }\n if (presentation)\n throw new Error(`${at} has \"opens\" and a ${presentation.kind} presentation; a row opens another record from a grid only.`);\n }\n if (presentation?.kind === 'split' && v.open !== undefined) {\n throw new Error(`${at} has \"open\" and a split presentation, which opens its rows in its own detail pane. Remove \"open\".`);\n }\n let actions: string[] | undefined;\n if (v.actions !== undefined) {\n if (!Array.isArray(v.actions)) throw new Error(`${at} has \"actions\" that are not a list of action ids.`);\n if (presentation?.kind !== 'queue' && presentation?.kind !== 'board' && presentation?.kind !== 'custom') {\n throw new Error(\n `${at} has \"actions\", which only a queue, a board or a custom list takes: a grid, a checklist or a split draws no actions, and a record page offers every one of resources.${table}.actions.`,\n );\n }\n const declared = (resource.actions ?? []).map((action) => action.id);\n actions = v.actions.map((id, i) => {\n if (typeof id !== 'string' || !id.trim()) {\n throw new Error(`${at}, \"actions\" entry ${i + 1} is not an action id: a view lists the ids of resources.${table}.actions.`);\n }\n if (!declared.includes(id.trim())) {\n throw new Error(\n `${at}, \"actions\" names \"${id.trim()}\", which resources.${table}.actions does not declare. ` +\n (declared.length ? `It declares: ${declared.join(', ')}.` : 'It declares no actions.'),\n );\n }\n return id.trim();\n });\n if (new Set(actions).size !== actions.length) throw new Error(`${at} has \"actions\" that name one action twice.`);\n }\n /* The list's order, one key: \"created_on desc\" is how it reads when\n nothing else says one, and a list of named orders is the Sort menu a\n reader chooses from, the first of them the order the list opens in.\n Written like the query bar, so a chosen one is the query's own sort\n clause and the address carries it; the opening one is kept out of the\n bar, as a hand order is: it is the view's reading, not a question\n anyone asked. A tab's sort clause wins over it, and the menu over both.\n A hand order already is one, and a sort in the standing query would\n read every tab its own way. */\n let sort: string | undefined;\n let sorts: Array<{ label: string; sort: string }> | undefined;\n if (v.sort !== undefined) {\n if (Array.isArray(v.sort)) {\n if (v.sort.length < 2 || v.sort.length > 8)\n throw new Error(`${at} has a \"sort\" list that is not two to eight { label, sort }: one order is a string, \"created_on desc\".`);\n sorts = v.sort.map((entry, i) => {\n const here = `${at}, sort ${i + 1}`;\n if (!isRecord(entry)) throw new Error(`${here} is not an object { label, sort }.`);\n allowKeys(entry, ['label', 'sort'], here);\n if (typeof entry.label !== 'string' || !entry.label.trim() || entry.label.trim().length > 40)\n throw new Error(`${here} needs a \"label\" of at most 40 characters.`);\n if (typeof entry.sort !== 'string' || !SORT_TEXT.test(entry.sort)) {\n throw new Error(`${here} has a \"sort\" that is not written like the query bar: \"price\", \"price desc\", \"year desc, mileage\".`);\n }\n return {\n label: entry.label.trim(),\n sort: entry.sort.trim().replace(/\\s+/g, ' '),\n };\n });\n if (new Set(sorts.map((entry) => entry.label)).size !== sorts.length)\n throw new Error(`${at} has a \"sort\" list with one label twice.`);\n sort = sorts[0]!.sort;\n } else if (typeof v.sort !== 'string' || !SORT_TEXT.test(v.sort)) {\n throw new Error(\n `${at} has a \"sort\" that is not written like the query bar (\"created_on desc\", \"year desc, mileage\") or a list of named orders, [{ label: 'Newest', sort: 'listed_on desc' }, …].`,\n );\n } else sort = v.sort.trim().replace(/\\s+/g, ' ');\n if (v.order !== undefined) {\n throw new Error(\n `${at} has \"sort\" and \"order\": a hand order is the list's own reading already, kept in \"${String(v.order).trim()}\". Keep one.`,\n );\n }\n if (presentation?.kind === 'board' || presentation?.kind === 'calendar') {\n throw new Error(`${at} has \"sort\" and a ${presentation.kind} presentation, which orders its cards itself.`);\n }\n }\n /* A hand order: the numeric column each row's place is kept in, read\n ascending wherever nothing else sorts the question. A sort in the\n standing query would win on every tab, so the order could never be read\n and is refused; a tab's own sort, a header and the Sort menu are readings\n of their own, and hide the handles while they last. */\n let order: string | undefined;\n if (v.order !== undefined) {\n if (typeof v.order !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(v.order.trim())) {\n throw new Error(`${at}: \"order\" must name one of the resource's own numeric columns, holding each row's place in the list.`);\n }\n order = v.order.trim();\n if (presentation?.kind === 'board') {\n throw new Error(`${at} has \"order\" and a board presentation; a board keeps its cards' places in its presentation's \"order\".`);\n }\n if (\n presentation?.kind === 'calendar' ||\n presentation?.kind === 'split' ||\n presentation?.kind === 'tree' ||\n presentation?.kind === 'gantt'\n ) {\n const which = {\n calendar: 'puts each record on its day',\n split: 'reads its list beside a record',\n tree: 'puts each record under its parent',\n gantt: 'puts each record on its dates',\n }[presentation.kind];\n throw new Error(\n `${at} has \"order\" and a ${presentation.kind} presentation, which ${which}. ` +\n 'A hand order is for a grid, a checklist, a queue or cards.',\n );\n }\n if (resource?.machine?.column === order) {\n throw new Error(\n `${at}: \"order\" names \"${order}\", which the machine of resources.${table} moves. ` +\n \"A row's place is the reader's to change; name another numeric column.\",\n );\n }\n }\n /* Single keys for the picked record's actions, in a split: the pane's header shows each beside its verb.\n Authored `shortcuts`; kept as the view's `keys`. */\n let keys: Record<string, string> | undefined;\n if (v.shortcuts !== undefined) {\n if (presentation?.kind !== 'split')\n throw new Error(`${at} has \"shortcuts\", which a split takes: they act on the record picked beside the list.`);\n if (!isRecord(v.shortcuts) || !Object.keys(v.shortcuts).length)\n throw new Error(`${at}: \"shortcuts\" must be an object of one key to an action id: { e: 'escalate' }.`);\n const declared = (resource?.actions ?? []).map((action) => action.id);\n keys = {};\n for (const [key, id] of Object.entries(v.shortcuts)) {\n if (!/^[a-z0-9]$/.test(key) || key === 'j' || key === 'k') {\n throw new Error(`${at}, \"shortcuts\" has \"${key}\": a key is one lowercase letter or digit, and J and K are the walk's own.`);\n }\n if (typeof id !== 'string' || !declared.includes(id.trim())) {\n throw new Error(\n `${at}, \"shortcuts\" maps \"${key}\" to ${JSON.stringify(id)}, which resources.${table}.actions does not declare. ` +\n (declared.length ? `It declares: ${declared.join(', ')}.` : 'It declares no actions.'),\n );\n }\n keys[key] = id.trim();\n }\n if (new Set(Object.values(keys)).size !== Object.keys(keys).length)\n throw new Error(`${at} has \"shortcuts\" that give one action two keys.`);\n }\n if (v.heading !== undefined && v.heading !== false) throw new Error(`${at}: \"heading\" is false, to draw the list without its title.`);\n /* A standing query is the conditions every tab reads under: the order is `sort`, the columns `show`. */\n const standingQuery = optionalText(v, 'query', at);\n if (standingQuery !== undefined) conditionsOnly(standingQuery, at);\n return {\n table,\n ...(sorts ? { sorts } : {}),\n ...(sort ? { sort } : {}),\n ...(order ? { order } : {}),\n ...(v.label === undefined ? {} : { label: parseCaption(v.label, `${at}.label`) }),\n ...(v.path === undefined ? {} : { path: parseRoutePath(v.path, at) }),\n ...(v.icon === undefined ? {} : { icon: parseIcon(v.icon, at) }),\n ...(v.realm === undefined ? {} : { realm: parseRealm(v.realm, at) }),\n ...(standingQuery === undefined ? {} : { query: standingQuery }),\n ...(v.show === undefined ? {} : { show: parseShow(v.show, at) }),\n ...(presentation ? { presentation } : {}),\n ...(empty ? { empty } : {}),\n ...(v.tabs === undefined ? {} : { tabs: parseTabs(v.tabs, at) }),\n ...(filters === undefined ? {} : { filters }),\n ...(v.filterBar === undefined ? {} : { filterBar: v.filterBar as FilterBar }),\n ...(suggest ? { suggest } : {}),\n ...(v.advancedFilters === undefined ? {} : { advancedFilters: v.advancedFilters as boolean }),\n ...(v.quickAdd === true ? { quickAdd: true as const } : {}),\n ...(favoriteFlag(v.favorite, at) ? { favorite: true as const } : {}),\n ...(totals ? { totals } : {}),\n ...(v.density === undefined ? {} : { density: v.density as 'comfortable' | 'compact' }),\n ...(v.rowActions === true ? { rowActions: true as const } : {}),\n ...(editable ? { editable } : {}),\n ...(v.open === undefined ? {} : { open: v.open as AppListView['open'] }),\n ...(v.opens === undefined ? {} : { opens: (v.opens as string).trim() }),\n ...(v.sizing === undefined ? {} : { sizing: parseTableSizing(v.sizing, at) }),\n ...(actions ? { actions } : {}),\n ...(importing === undefined ? {} : { import: importing }),\n ...(keys ? { keys } : {}),\n ...(v.heading === false ? { heading: false as const } : {}),\n ...(countFlag(v.count, at) === undefined ? {} : { count: countFlag(v.count, at) }),\n };\n}\n\n/* A list's say on its menu count (shared/menuCounts.ts): true or false over the rule. */\nfunction countFlag(value: unknown, at: string): boolean | undefined {\n if (value === undefined) return undefined;\n if (typeof value !== 'boolean') throw new Error(`${at}: \"count\" is true or false: whether the menu counts this list.`);\n return value;\n}\n\n/**\n * A view's `show`: plain strings, each a column or a dotted label path. The\n * caption is the resource's (`resources.<table>.labels`), wherever it shows.\n */\nfunction parseShow(raw: unknown, at: string): string[] {\n if (!Array.isArray(raw)) throw new Error(`${at} has a \"show\" that is not a list of column names.`);\n return raw.map((entry, i) => {\n if (typeof entry !== 'string' || !entry.trim()) throw new Error(`${at}, \"show\" entry ${i + 1} is not a column name or a label path.`);\n return entry.trim();\n });\n}\n\n/** What a list that names nothing is called: its records, several of them, as the resource's noun says, else its table's name. */\nfunction listLabel(table: string, data: ParsedData): string {\n const noun = data.resources?.[table]?.noun;\n return noun ? headingOf(noun.many) : humanName(table);\n}\n\n/** A dashboard section's captions: the resource's word for each shown column it has one for. */\nfunction labelsFor(show: readonly string[] | undefined, labels: Record<string, string> | undefined): Record<string, string> | undefined {\n const named = (show ?? []).filter((column) => labels !== undefined && Object.hasOwn(labels, column));\n return named.length ? Object.fromEntries(named.map((column) => [column, labels![column]])) : undefined;\n}\n\nfunction parseTabs(raw: unknown, at: string): AppListTab[] {\n if (!Array.isArray(raw)) throw new Error(`${at} has \"tabs\" that are not an array.`);\n return raw.map((entry, i) => {\n const here = `${at}, tab ${i + 1}`;\n if (!isRecord(entry)) throw new Error(`${here} is not an object.`);\n /* A tab's own order is its `sort`, kept as a sort clause after its conditions —\n \"status = open sort due_on\" — which every reader of the tab (its\n count, the address, a dashboard's embed) takes as it is, and which\n wins over the view's `sort`. */\n // `segment` is what a tab naming one unfolds to (appSegments.ts); its name is the tab's address.\n allowKeys(entry, ['label', 'query', 'segment', 'sort', 'empty'], here);\n if (typeof entry.label !== 'string' || !entry.label.trim()) throw new Error(`${here} has no \"label\".`);\n const query = optionalText(entry, 'query', here);\n const segment = typeof entry.segment === 'string' ? entry.segment : undefined;\n if (query !== undefined) conditionsOnly(query, here);\n if (entry.sort !== undefined && (typeof entry.sort !== 'string' || !SORT_TEXT.test(entry.sort))) {\n throw new Error(`${here} has a \"sort\" that is not written like the query bar: \"due_on\", \"created_on desc\", \"year desc, mileage\".`);\n }\n const sort = typeof entry.sort === 'string' ? entry.sort.trim().replace(/\\s+/g, ' ') : undefined;\n /* What the tab says while nothing is under it, as a list's `empty` says it for the list as it opens. */\n const empty = entry.empty === undefined ? undefined : parseListEmpty(entry.empty, here);\n return {\n label: entry.label.trim(),\n query: sort === undefined ? query : `${query ? `${query} ` : ''}sort ${sort}`,\n ...(segment ? { segment, slug: segment } : {}),\n ...(empty ? { empty } : {}),\n };\n });\n}\n\n/** A local column for a card: bare, non-empty, never a path. */\nfunction localColumn(o: Record<string, unknown>, key: string, at: string): string {\n const value = o[key];\n if (typeof value !== 'string' || !value.trim() || value.includes('.'))\n throw new Error(`${at}: \"${key}\" must name a local column, not a path.`);\n return value.trim();\n}\n\n/** A card's title: its own column, or one read through a reference, one hop (`vehicle_id.title`). */\nfunction cardTitle(value: unknown, at: string): string {\n if (typeof value !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*(\\.[A-Za-z_][A-Za-z0-9_]*)?$/.test(value.trim()))\n throw new Error(`${at}: \"primary\" must be a local column or a one-hop path like \"vehicle_id.title\".`);\n return value.trim();\n}\n\n/** A queue may say a referenced record's name, but never walk past one join. */\nfunction queueColumn(value: unknown, key: string, at: string): string {\n if (typeof value !== 'string' || !value.trim()) {\n throw new Error(`${at}: \"${key}\" must be a local column or a one-hop label path like \"employee_id.name\".`);\n }\n const name = value.trim();\n /* Keep legacy local names verbatim (apart from outer whitespace): before\n paths, a local column was any non-empty spelling without a dot. */\n if (!name.includes('.')) return name;\n if (/^[A-Za-z_][A-Za-z0-9_]*\\.[A-Za-z_][A-Za-z0-9_]*$/.test(name)) return name;\n throw new Error(`${at}: \"${key}\" must be a local column or a one-hop label path like \"employee_id.name\".`);\n}\n\n/**\n * A calendar's shaded spans: one kind or a list of them, each another\n * resource's rows from one of its date columns to another, labeled by a third\n * and shaded in a display tone. The resource is checked as declared where the\n * view is (parseListView), and its columns against the catalog (listProblem).\n */\nfunction parseCalendarBackdrop(raw: unknown, at: string): AppCalendarBackdrop[] {\n const list = Array.isArray(raw) ? raw : [raw];\n if (!list.length || list.length > CALENDAR_BACKDROPS_MAX) {\n throw new Error(\n `${at}: a calendar shades one to ${CALENDAR_BACKDROPS_MAX} kinds of span, like { from: 'blackout', start: 'starts_on', end: 'ends_on', labelColumn: 'name' }.`,\n );\n }\n return list.map((entry, i) => {\n const here = list.length > 1 ? `${at} ${i + 1}` : at;\n if (!isRecord(entry)) throw new Error(`${here} is not { from, start, end, labelColumn }.`);\n allowKeys(entry, ['from', 'start', 'end', 'labelColumn', 'tone'], here, BACKDROP_RENAMED);\n if (typeof entry.from !== 'string' || !entry.from.trim())\n throw new Error(`${here} has no \"from\": the resource whose rows are the spans, like 'blackout'.`);\n const start = localColumn(entry, 'start', here);\n const end = localColumn(entry, 'end', here);\n const labelColumn = localColumn(entry, 'labelColumn', here);\n if (entry.tone !== undefined && !(DISPLAY_TONES as readonly unknown[]).includes(entry.tone)) {\n throw new Error(`${here} has a \"tone\" of \"${String(entry.tone)}\". Use ${DISPLAY_TONES.join(', ')}.`);\n }\n return {\n from: entry.from.trim(),\n start,\n end,\n labelColumn,\n ...(entry.tone === undefined ? {} : { tone: entry.tone as AppCalendarBackdrop['tone'] }),\n };\n });\n}\n\n/**\n * How a list's rows draw: a checklist, a queue, a board — each a card of one\n * `primary` column and up to its `lines` — or a split list/detail workspace.\n * Local readable columns only for the cards, refused by name, so an author\n * learns one grammar.\n */\n/** Shared shape validation for authored lists and captured App embeds. */\nexport function parsePresentation(raw: unknown, at: string): AppListPresentation {\n const here = `${at}, presentation`;\n if (!isRecord(raw)) throw new Error(`${here} must be a checklist, queue, board, calendar, tree, gantt, cards, split or custom object.`);\n const kind = raw.kind;\n /* The author's own component draws the rows; the list around them is the platform's. */\n if (kind === 'custom') return parseCustomList(raw, `${here} (a custom list)`);\n if (\n kind !== 'checklist' &&\n kind !== 'queue' &&\n kind !== 'board' &&\n kind !== 'calendar' &&\n kind !== 'tree' &&\n kind !== 'gantt' &&\n kind !== 'cards' &&\n kind !== 'split'\n ) {\n throw new Error(`${here} needs a \"kind\" of \"checklist\", \"queue\", \"board\", \"calendar\", \"tree\", \"gantt\", \"cards\", \"split\" or \"custom\".`);\n }\n allowKeys(raw, PRESENTATION_KEYS[kind], `${here} (a ${kind})`, PRESENTATION_RENAMED[kind as keyof typeof PRESENTATION_RENAMED]);\n if (kind === 'split') return parseSplit(raw, `${here} (a split)`);\n if (kind === 'gantt') {\n const start = localColumn(raw, 'start', here);\n const end = localColumn(raw, 'end', here);\n if (end === start) throw new Error(`${here}: \"end\" names \"${end}\", the same column as \"start\"; a bar needs the column it ends on.`);\n return {\n kind,\n start,\n end,\n primary: queueColumn(raw.primary, 'primary', here),\n ...(raw.by === undefined ? {} : { by: queueColumn(raw.by, 'by', here) }),\n ...(raw.progress === undefined ? {} : { progress: localColumn(raw, 'progress', here) }),\n };\n }\n /* A card with no title of its own is titled by its record's name, `$label`, which resolving the view against\n the catalog makes the path that name is read from (appCards.ts cardsTitled). */\n /* A board, a queue or a calendar with no `primary` says the record's name too: resolving the list against the\n catalog reads it where the resource names its records (app.ts titledPresentation). */\n const primary =\n (kind === 'board' || kind === 'queue' || kind === 'calendar') && raw.primary === undefined\n ? LABEL_SEGMENT\n : kind === 'queue' || kind === 'calendar' || kind === 'tree' || kind === 'board'\n ? queueColumn(raw.primary, 'primary', here)\n : kind === 'cards' && raw.primary === undefined\n ? LABEL_SEGMENT\n : kind === 'cards'\n ? cardTitle(raw.primary, here)\n : localColumn(raw, 'primary', here);\n /* A card's captioned values are its `properties`; every other presentation's quiet lines under the title are `lines`.\n The model keeps one list for both. */\n const linesKey = kind === 'cards' ? 'properties' : 'lines';\n const rawLines = raw[linesKey];\n let lines: string[] = [];\n if (rawLines !== undefined) {\n const max = MAX_LINES[kind];\n if (!Array.isArray(rawLines) || rawLines.length > max) {\n throw new Error(`${here}: \"${linesKey}\" must be a list of at most ${max} local column${max === 1 ? '' : 's'} beneath the primary.`);\n }\n /* Cards, queues and calendars may say a referenced record's name; the\n other presentations' lines stay the row's own columns. */\n lines = rawLines.map((_, i) => {\n const line = (rawLines as unknown[])[i];\n if (kind === 'queue' || kind === 'calendar' || kind === 'tree') return queueColumn(line, String(i), `${here}, lines`);\n if (kind !== 'cards') return localColumn(rawLines as unknown as Record<string, unknown>, String(i), `${here}, lines`);\n if (typeof line !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*(\\.[A-Za-z_][A-Za-z0-9_]*)?$/.test(line.trim())) {\n throw new Error(`${here}, properties: a card's property is a local column or a one-hop label path like \"dealer_id.name\".`);\n }\n return line.trim();\n });\n }\n if (kind === 'checklist')\n return {\n kind,\n primary,\n checked: localColumn(raw, 'checked', here),\n ...(lines.length ? { lines } : {}),\n };\n if (kind === 'tree')\n return {\n kind,\n parent: localColumn(raw, 'parent', here),\n primary,\n ...(lines.length ? { lines } : {}),\n };\n if (kind === 'queue') {\n let facts: string[] | undefined;\n if (raw.properties !== undefined) {\n if (!Array.isArray(raw.properties) || !raw.properties.length || raw.properties.length > 6) {\n throw new Error(`${here}: \"properties\" must be one to 6 local columns or one-hop label paths.`);\n }\n facts = raw.properties.map((fact, i) => queueColumn(fact, String(i), `${here}, properties`));\n }\n const badge = raw.badge === undefined ? undefined : queueColumn(raw.badge, 'badge', here);\n const openLabel = raw.openLabel;\n if (openLabel !== undefined && (typeof openLabel !== 'string' || !openLabel.trim())) {\n throw new Error(`${here}: \"openLabel\" must be a non-empty string.`);\n }\n return {\n kind,\n primary,\n ...(lines.length ? { lines } : {}),\n ...(facts?.length ? { facts } : {}),\n ...(badge === undefined ? {} : { badge }),\n ...(typeof openLabel === 'string' ? { openLabel: openLabel.trim() } : {}),\n };\n }\n if (kind === 'calendar') {\n const date = localColumn(raw, 'date', here);\n /* A range: the record spans its days from `date` to `end`, both its own\n columns, so a move can shift the two together. */\n const end = raw.end === undefined ? undefined : localColumn(raw, 'end', here);\n if (end !== undefined && end === date)\n throw new Error(`${here}: \"end\" names \"${end}\", the same column as \"date\"; a range needs the column its last day is in.`);\n const badge = raw.badge === undefined ? undefined : queueColumn(raw.badge, 'badge', here);\n const backdrop = raw.backdrop === undefined ? undefined : parseCalendarBackdrop(raw.backdrop, `${here}, backdrop`);\n return {\n kind,\n date,\n ...(end === undefined ? {} : { end }),\n primary,\n ...(lines.length ? { lines } : {}),\n ...(badge === undefined ? {} : { badge }),\n ...(backdrop ? { backdrop } : {}),\n };\n }\n if (kind === 'cards') return parseCards(raw, `${here} (cards)`, primary, lines);\n const by = localColumn(raw, 'by', here);\n let lanes: string[] | undefined;\n if (raw.lanes !== undefined) {\n if (\n !Array.isArray(raw.lanes) ||\n !raw.lanes.length ||\n raw.lanes.length > 8 ||\n raw.lanes.some((lane) => typeof lane !== 'string' || !lane.trim())\n ) {\n throw new Error(`${here}: \"lanes\" must be one to eight non-empty lane values.`);\n }\n lanes = (raw.lanes as string[]).map((lane) => lane.trim());\n if (new Set(lanes).size !== lanes.length) throw new Error(`${here}: \"lanes\" must be unique.`);\n }\n const order = raw.order === undefined ? undefined : localColumn(raw, 'order', here);\n const swimlanes = raw.swimlanes === undefined ? undefined : localColumn(raw, 'swimlanes', here);\n /* A row, a lane and a place are three questions; one column cannot answer two of them. */\n if (swimlanes !== undefined && (swimlanes === by || swimlanes === order)) {\n throw new Error(\n `${here}: \"swimlanes\" names \"${swimlanes}\", which is already the board's \"${swimlanes === by ? 'by' : 'order'}\". Rows need a column of their own.`,\n );\n }\n let limits: Record<string, number> | undefined;\n if (raw.limits !== undefined) {\n if (!isRecord(raw.limits) || !Object.keys(raw.limits).length) {\n throw new Error(`${here}: \"limits\" must be an object of lane values to whole numbers, like { in_progress: 4 }.`);\n }\n limits = {};\n for (const [key, n] of Object.entries(raw.limits)) {\n const lane = key.trim();\n if (!lane) throw new Error(`${here}: \"limits\" has a blank lane value.`);\n if (lanes && !lanes.includes(lane))\n throw new Error(`${here}: \"limits\" names \"${lane}\", which is not one of its \"lanes\" (${lanes.join(', ')}).`);\n if (typeof n !== 'number' || !Number.isInteger(n) || n < 1 || n > 999) {\n throw new Error(`${here}: \"limits.${lane}\" must be a whole number from 1 to 999: the lane's work-in-progress limit.`);\n }\n limits[lane] = n;\n }\n }\n const category = raw.category === undefined ? undefined : localColumn(raw, 'category', here);\n if (category !== undefined && category === by)\n throw new Error(`${here}: \"category\" names \"${category}\", the board's \"by\": every card in a lane would wear the lane's chip.`);\n const footer = raw.footer === undefined ? undefined : parseBoardFooter(raw.footer, here);\n const sum = raw.sum === undefined ? undefined : localColumn(raw, 'sum', here);\n if (sum !== undefined && sum === by)\n throw new Error(`${here}: \"sum\" names \"${sum}\", the board's \"by\": a lane's own value is not an amount.`);\n let folded: string[] | undefined;\n if (raw.folded !== undefined) {\n if (\n !Array.isArray(raw.folded) ||\n !raw.folded.length ||\n raw.folded.length > 8 ||\n raw.folded.some((lane) => typeof lane !== 'string' || !lane.trim())\n )\n throw new Error(`${here}: \"folded\" must be one to eight lane values, like ['won', 'lost'].`);\n folded = (raw.folded as string[]).map((lane) => lane.trim());\n const stray = lanes && folded.find((lane) => !lanes!.includes(lane));\n if (stray) throw new Error(`${here}: \"folded\" names \"${stray}\", which is not one of its \"lanes\" (${lanes!.join(', ')}).`);\n }\n return {\n kind,\n by,\n ...(lanes ? { lanes } : {}),\n ...(order === undefined ? {} : { order }),\n ...(swimlanes === undefined ? {} : { swimlanes }),\n ...(limits ? { limits } : {}),\n primary,\n ...(lines.length ? { lines } : {}),\n ...(category === undefined ? {} : { category }),\n ...(footer ? { footer } : {}),\n ...(sum === undefined ? {} : { sum }),\n ...(folded ? { folded: [...new Set(folded)] } : {}),\n };\n}\n\n/** A card's foot: one to four local columns, each bare or `{ column, icon }`. */\nfunction parseBoardFooter(raw: unknown, here: string): AppBoardFact[] {\n if (!Array.isArray(raw) || !raw.length || raw.length > 4)\n throw new Error(`${here}: \"footer\" must be one to four local columns, each a name or { column, icon }.`);\n const facts = raw.map((item, index): AppBoardFact => {\n const at = `${here}, footer[${index}]`;\n if (typeof item === 'string') return { column: localColumn({ column: item }, 'column', at) };\n if (!isRecord(item)) throw new Error(`${at} must be a local column or { column, icon }.`);\n allowKeys(item, ['column', 'icon'], at);\n return {\n column: localColumn(item, 'column', at),\n ...(item.icon === undefined ? {} : { icon: parseIcon(item.icon, at) }),\n };\n });\n const seen = new Set<string>();\n for (const fact of facts) {\n if (seen.has(fact.column)) throw new Error(`${here}: \"footer\" names \"${fact.column}\" twice.`);\n seen.add(fact.column);\n }\n return facts;\n}\n\n/**\n * A split draws the view's own list — its show, query, tabs and filter bar —\n * beside the record the reader picks, so all it says itself is the detail's\n * fields and the list's page size. The detail is headed by the resource's\n * noun, as New is.\n */\nfunction parseSplit(input: Record<string, unknown>, at: string): AppSplitListDetail {\n const result: AppSplitListDetail = { kind: 'split' };\n if (input.detail !== undefined) {\n if (!Array.isArray(input.detail) || input.detail.length > 64) {\n throw new Error(`${at}: \"detail\" must be a list of at most 64 columns or label paths: the picked record's fields.`);\n }\n const detail = parseShow(input.detail, `${at}, detail`);\n if (new Set(detail).size !== detail.length) throw new Error(`${at}: \"detail\" names one column twice.`);\n result.detail = detail;\n }\n if (input.limit !== undefined) {\n if (!Number.isInteger(input.limit) || (input.limit as number) < 1 || (input.limit as number) > 200) {\n throw new Error(`${at}: \"limit\" must be an integer from 1 to 200: the list's page size.`);\n }\n result.limit = input.limit as number;\n }\n if (input.row !== undefined) {\n /* The queue's vocabulary: a split's rows are the queue's rows, narrowed to the pane beside a record. */\n const here = `${at}, \"row\"`;\n if (!isRecord(input.row)) throw new Error(`${here} must be an object: { primary, lines, properties, badge, avatar }.`);\n allowKeys(input.row, ['primary', 'lines', 'properties', 'badge', 'avatar'], here, PRESENTATION_RENAMED.queue);\n const row = input.row;\n const primary = queueColumn(row.primary, 'primary', here);\n if (row.lines !== undefined && (!Array.isArray(row.lines) || row.lines.length > MAX_LINES.queue)) {\n throw new Error(`${here}: \"lines\" must be a list of at most ${MAX_LINES.queue} columns beneath the primary.`);\n }\n const lines = ((row.lines ?? []) as unknown[]).map((line, i) => queueColumn(line, String(i), `${here}, lines`));\n if (row.properties !== undefined && (!Array.isArray(row.properties) || !row.properties.length || row.properties.length > 6)) {\n throw new Error(`${here}: \"properties\" must be one to 6 local columns or one-hop label paths.`);\n }\n const facts = ((row.properties ?? []) as unknown[]).map((fact, i) => queueColumn(fact, String(i), `${here}, properties`));\n result.row = {\n primary,\n ...(lines.length ? { lines } : {}),\n ...(facts.length ? { facts } : {}),\n ...(row.badge === undefined ? {} : { badge: queueColumn(row.badge, 'badge', here) }),\n ...(row.avatar === undefined ? {} : { avatar: queueColumn(row.avatar, 'avatar', here) }),\n };\n }\n return result;\n}\n\n/* ---------- dashboard views ---------- */\n\nfunction parseDashboardView(\n v: Record<string, unknown>,\n key: string,\n at: string,\n data: ParsedData,\n views: ReadonlyMap<string, ParsedView>,\n signsIn: boolean,\n): DashboardView {\n allowKeys(v, [...DASHBOARD_VIEW_KEYS, 'realm'], at, DASHBOARD_VIEW_RENAMED);\n if (typeof v.label !== 'string' || !v.label.trim()) throw new Error(`${at} is a dashboard and has no \"label\".`);\n /* A dashboard-level \"from\" is the sections' default — a board over one\n table should not repeat its name six times. A section that names its\n own still wins. */\n const description = optionalText(v, 'description', at);\n /* The words over the sections when they are not the menu's: \"Hello, {me.name}\". */\n /* false: the title is not drawn (it stays for a screen reader); the menu and the tab keep the label. */\n const heading = v.heading === false ? undefined : optionalText(v, 'heading', at);\n checkGreeting(heading, `${at}, \"heading\"`, signsIn);\n checkGreeting(description, `${at}, \"description\"`, signsIn);\n const table = optionalText(v, 'from', at);\n if (table) {\n checkResourceSlot(table, `${at}, \"from\"`, data);\n checkDeclaredResource(table, `${at}, \"from\"`, data);\n }\n if (\n v.windows !== undefined &&\n (!Array.isArray(v.windows) || !v.windows.length || v.windows.some((w) => typeof w !== 'string' || !w.trim()))\n ) {\n throw new Error(`${at} has \"windows\" that are not a list of phrases like \"last 30 days\".`);\n }\n /* The window it opens on, one of the windows it offers; an address's `?period=` still wins. */\n const offered = ((v.windows as string[] | undefined) ?? DEFAULT_WINDOWS).map((w) => w.trim().replace(/\\s+/g, ' '));\n const opensOn = typeof v.window === 'string' ? v.window.trim().replace(/\\s+/g, ' ') : undefined;\n if (v.window !== undefined && (!opensOn || !offered.includes(opensOn)))\n throw new Error(\n `${at} has a \"window\" of ${JSON.stringify(v.window)}, which is not one of the windows it offers: ${offered.map((w) => JSON.stringify(w)).join(', ')}.`,\n );\n if (v.windowControl !== undefined && v.windowControl !== 'segmented' && v.windowControl !== 'button')\n throw new Error(`${at} has a \"windowControl\" of ${JSON.stringify(v.windowControl)}. Use \"segmented\" or \"button\".`);\n if (v.filters !== undefined && (!Array.isArray(v.filters) || v.filters.some((f) => typeof f !== 'string' || !f.trim()))) {\n throw new Error(`${at} has \"filters\" that are not a list of column names.`);\n }\n if (!Array.isArray(v.sections)) throw new Error(`${at} needs \"sections\": a list of at least one section.`);\n if (v.aside !== undefined && !Array.isArray(v.aside)) throw new Error(`${at} has an \"aside\" that is not a list of sections.`);\n const embeds: string[] = [];\n const sections = parseDashSections(v.sections, at, table, data, views, embeds);\n const aside = Array.isArray(v.aside) ? parseDashSections(v.aside, `${at}, \"aside\"`, table, data, views, embeds, true) : undefined;\n nameSectionWindows([...sections, ...(aside ?? [])], at);\n const areas = parseAreas(v.areas, at, sections, aside !== undefined);\n /* The sections' default is one only while a section inherits it: a table\n every section names again for itself is read by nothing. */\n if (\n table &&\n ![...(v.sections as unknown[]), ...((v.aside as unknown[] | undefined) ?? [])].some(\n (s) =>\n isRecord(s) &&\n s.kind !== 'note' &&\n s.kind !== 'view' &&\n s.kind !== 'firstRun' &&\n s.kind !== 'activity' &&\n s.kind !== 'find' &&\n s.kind !== 'branding' &&\n s.kind !== 'integrations' &&\n s.from === undefined,\n )\n ) {\n throw new Error(`${at} has a \"from\" that no section inherits: every section names its own \"from\". Remove it.`);\n }\n return {\n kind: 'dashboard',\n key,\n at,\n embeds,\n tables: [...new Set([...sections, ...(aside ?? [])].map((section) => section.table).filter((name): name is string => Boolean(name)))],\n view: {\n label: v.label.trim(),\n ...(heading === undefined ? {} : { heading }),\n ...(v.heading === false ? { headingHidden: true as const } : {}),\n ...(description === undefined ? {} : { description }),\n sections,\n ...(aside === undefined ? {} : { aside }),\n ...(areas === undefined ? {} : { areas }),\n ...(v.path === undefined ? {} : { path: parseRoutePath(v.path, at) }),\n ...(v.icon === undefined ? {} : { icon: parseIcon(v.icon, at) }),\n ...(v.realm === undefined ? {} : { realm: parseRealm(v.realm, at) }),\n ...(v.windows === undefined ? {} : { windows: (v.windows as string[]).map((w) => w.trim()) }),\n ...(opensOn === undefined ? {} : { window: opensOn }),\n ...(v.windowControl === 'button' ? { windowControl: 'button' as const } : {}),\n ...(v.filters === undefined ? {} : { filters: (v.filters as string[]).map((f) => f.trim()) }),\n ...(favoriteFlag(v.favorite, at) ? { favorite: true as const } : {}),\n },\n };\n}\n\n/**\n * The keys of a free-form page as the App loader writes it from a\n * `pages/*.page.tsx` file (packages/mistri/src/server/app/appPages.ts) — never authored in\n * `views`, which refuses the kind. `heading` and `props` are the internal\n * Console's, whose pages are written as data.\n */\nconst PAGE_VIEW_KEYS = [\n 'kind',\n 'label',\n 'icon',\n 'description',\n 'heading',\n 'favorite',\n 'component',\n 'reads',\n 'commands',\n 'params',\n 'loader',\n 'props',\n 'roles',\n 'public',\n] as const;\n\n/**\n * A free-form page: the whole page drawn by the author's component. It is\n * served as a dashboard of one full-width custom section — so its menu\n * entry, its home rule, its address and its favorite star are a\n * dashboard's, with nothing to learn twice — which says `page`: the\n * resources it reads, which gate it like a section's table gates a\n * dashboard (a reader who cannot read one is not served the page), the\n * commands it runs, the address parameters it keeps its place in and the\n * loader that reads for it on the server.\n *\n * Who is offered it is said once, here, by what it reads: every role a page\n * names must be granted every read, or the page is a mistake some role would\n * meet broken; one that names none is offered to the people's roles that\n * are — never a service role, which no person holds — and never to a\n * signed-out visitor unless it says `public: true`, when every read must be\n * one `auth.public` opens. A command is not a door: a reader the page is\n * offered to who may not run one of its commands meets the page without it\n * (the command's handle says `granted: false`, and the server refuses it as\n * it refuses any command not granted). A command no role offered the page\n * may run is a mistake, as an unreadable read is.\n */\nfunction parsePageView(v: Record<string, unknown>, key: string, at: string, ctx: ContentContext): DashboardView {\n allowKeys(v, PAGE_VIEW_KEYS, `${at} (a free-form page)`);\n if (typeof v.label !== 'string' || !v.label.trim()) throw new Error(`[page-file] ${at} is a free-form page and has no \"title\".`);\n const page = parsePage(v, at);\n for (const resource of page.reads) {\n checkResourceSlot(resource, `${at}, \"reads\"`, ctx.data);\n checkDeclaredResource(resource, `${at}, \"reads\"`, ctx.data);\n }\n for (const command of page.commands) {\n if (!isRegisteredCommand(command)) {\n throw new Error(`[page-commands] ${at} runs the command \"${command}\", which the commands module does not register.`);\n }\n }\n if (v.public !== undefined && v.public !== true) throw new Error(`[page-public] ${at} has a \"public\" that is not true.`);\n /* A public page's commands are run by a signed-out visitor through the page's own door (http/pageCommandRoutes.ts),\n as the public principal: only a command that is itself public, and one that makes its record rather than naming\n one, the way a public form's is. A signed-in page's need roles to be granted to. */\n if (v.public === true) {\n for (const command of page.commands) {\n const entry = registeredCommand(command);\n if (entry?.public !== true)\n throw new Error(\n `[page-commands] ${at} is public and runs \"${command}\", which is not public: a public page runs only commands that say public: true.`,\n );\n if (entry.targets !== 'none')\n throw new Error(`[page-commands] ${at} is public and runs \"${command}\", which names a record; a public command makes its own.`);\n }\n if (page.commands.length && !ctx.auth)\n throw new Error(`[page-commands] ${at} runs public commands, and a public command needs \"auth\" (with \"auth.public\").`);\n } else if (page.commands.length && !ctx.auth?.roles)\n throw new Error(`[page-commands] ${at} runs commands, and commands need \"auth\" and \"auth.roles\".`);\n const roles = pageRoles(v, page, at, ctx);\n const description = optionalText(v, 'description', at);\n if (v.heading !== undefined && v.heading !== false) throw new Error(`${at}: a page's \"heading\" is false, to draw it without its title.`);\n return {\n kind: 'dashboard',\n key,\n at,\n embeds: [],\n tables: page.reads,\n view: {\n ...(v.heading === false ? { headingHidden: true as const } : {}),\n label: v.label.trim(),\n ...(description === undefined ? {} : { description }),\n sections: [\n {\n kind: 'custom',\n component: page.component,\n props: page.props,\n width: 'full',\n },\n ],\n page: {\n reads: page.reads,\n commands: page.commands,\n params: page.params,\n ...(page.loader ? { loader: page.loader } : {}),\n ...(v.public === true ? { public: true as const } : {}),\n },\n ...(v.icon === undefined ? {} : { icon: parseIcon(v.icon, at) }),\n ...(favoriteFlag(v.favorite, at) ? { favorite: true as const } : {}),\n ...(roles ? { roles } : {}),\n },\n };\n}\n\n/**\n * The roles a free-form page is offered to. Named, each must be granted\n * what the page reads (`page-coverage`); unnamed, the roles that are, but\n * a service role. Either way each command it runs must be some offered\n * role's. A public page names none: it is everyone's, a signed-out\n * visitor's too, and reads only what `auth.public` opens.\n */\nfunction pageRoles(\n v: Record<string, unknown>,\n page: { reads: string[]; commands: string[] },\n at: string,\n ctx: ContentContext,\n): string[] | undefined {\n const grants = ctx.auth?.roles;\n if (v.public === true) {\n if (v.roles !== undefined) throw new Error(`[page-public] ${at} is public and names \"roles\": a public page is everyone's.`);\n const open = new Set(ctx.auth?.public?.read ?? []);\n if (!ctx.auth?.public)\n throw new Error(`[page-public] ${at} is public, and the App opens nothing to signed-out visitors (auth.public).`);\n const closed = page.reads.find((resource) => !open.has(resource));\n if (closed) throw new Error(`[page-public] ${at} is public and reads \"${closed}\", which \"auth.public\" does not open.`);\n return undefined;\n }\n if (!grants) return parseOpenRoles(v.roles, at, ctx.auth);\n const reads = (grant: AppRoleGrant, resource: string) =>\n grant.read === true || (Array.isArray(grant.read) && grant.read.includes(resource));\n const unread = (grant: AppRoleGrant): string | undefined => page.reads.find((resource) => !reads(grant, resource));\n /* Each command is some offered role's to run, or no one could: a mistake, as an unreadable read is. */\n const runnable = (offered: readonly string[]) => {\n const stray = page.commands.find((command) => !offered.some((role) => (grants[role].commands ?? []).includes(command)));\n if (stray !== undefined) {\n throw new Error(\n `[page-coverage] ${at} runs \"${stray}\", which no role it is offered to is granted. Grant it to one, or leave it out of \"commands\".`,\n );\n }\n };\n const named = parseOpenRoles(v.roles, at, ctx.auth);\n if (named) {\n for (const role of named) {\n const missing = unread(grants[role]);\n if (missing !== undefined) {\n throw new Error(\n `[page-coverage] ${at} is offered to the role \"${role}\", which is not granted \"${missing}\". ` +\n 'Every role a page names reads all it reads: grant it, or leave the role out.',\n );\n }\n }\n runnable(named);\n return named;\n }\n const able = Object.keys(grants).filter((role) => !grants[role].service && unread(grants[role]) === undefined);\n if (!able.length) {\n throw new Error(\n `[page-coverage] ${at} reads what no person's role is granted all of, so no one could open it. Grant a role, or narrow the page.`,\n );\n }\n runnable(able);\n return able;\n}\n\n/** A view with the roles it names, checked against the App's (appViewRoles.ts). */\nfunction withOpenRoles<V extends object>(view: V, raw: Record<string, unknown>, at: string, ctx: ContentContext): V & { roles?: string[] } {\n const roles = parseOpenRoles(raw.roles, at, ctx.auth);\n return roles ? { ...view, roles } : view;\n}\n\n/** A section kind as a noun: \"a metric\", \"a rows section\" — \"a rows\" is not English. */\n/** Keys a dashboard section kind took under another name, with what they are now. */\nconst RENAMED_SECTION_KEYS: Record<string, Record<string, string>> = {\n metric: { view: '\"to\": metric(title, from, { to: \\'overdue_list\\' }) opens the list view it names' },\n custom: {\n totals: '\"figures\": the named aggregate reads its component receives as figures.<name> (\"totals\" is a list\\'s footer of sums)',\n },\n};\n\nconst sectionWord = (kind: string) => (kind === 'rows' ? 'rows section' : kind);\n\n/** The target's standing filter stays ahead of a metric's own refinement,\n both when parsed and when its compare/window is derived (app.ts). */\nexport function metricCohortQuery(standingQuery: string | undefined, refinement: string | undefined): string | undefined {\n const standing = andQuery(splitClauseTail(standingQuery ?? '').head);\n const own = andQuery(splitClauseTail(refinement ?? '').head);\n // A normalized section may be parsed again. Only a whole leading\n // conjunction counts as already scoped; an `or` is bracketed above.\n return (standing && own !== standing && !own.startsWith(`${standing} and `) ? andQuery(standing, own) : own) || undefined;\n}\n\nfunction parseDashSections(\n raw: unknown[],\n at: string,\n defaultTable: string | undefined,\n data: ParsedData,\n views: ReadonlyMap<string, ParsedView>,\n embeds: string[],\n aside = false,\n): AppDashSection[] {\n if (!raw.length) throw new Error(`${at}: a dashboard needs at least one section.`);\n /* Each section is judged on its own: one run names every section's refusal (appProblems.ts). */\n const problems: string[] = [];\n const sections = raw.map((entry, i) =>\n gather(problems, (): AppDashSection => {\n const here = `${at}, section ${i + 1}`;\n if (!isRecord(entry)) throw new Error(`${here} is not an object.`);\n const s = entry;\n if (s.kind === 'view') {\n const section = embedSection(s, here, views, data, aside);\n embeds.push(s.view as string);\n return section;\n }\n const kind = String(s.kind ?? '');\n if (!(DASH_KINDS as readonly string[]).includes(kind)) {\n throw new Error(\n `${here} has an unknown kind \"${kind}\". Use \"metric\", \"breakdown\", \"rows\", \"chart\", \"note\", \"matrix\", \"funnel\", \"board\", \"firstRun\", \"custom\", \"activity\", \"find\", \"branding\", \"integrations\" or \"view\", each written with its helper: metric(), breakdown(), rows(), chart(), note(), matrix(), funnel(), board(), firstRun(), custom(), activity(), find(), brandingForm(), integrationsForm(), embed().`,\n );\n }\n if (kind === 'firstRun') {\n if (aside) throw new Error(`${here} is a first run, which stands in the dashboard's main flow: move it into \"sections\".`);\n if (raw.filter((other) => isRecord(other) && other.kind === 'firstRun').length > 1) {\n throw new Error(`${at} has more than one first run. One list of first steps is the start; put every step in it.`);\n }\n return firstRunSection(s, here, views);\n }\n /* Per kind, so `show` on a metric or `over` on a note is refused rather\n than read by nothing — `over` on a note used to switch the whole\n dashboard's window controls on for a section the window never reached.\n A key another kind reads is named as that kind's (\"only a chart\n carries\"); a key no kind reads is a typo and gets the list. */\n const takes = SECTION_KEYS[kind as AppDashSection['kind']];\n for (const key of Object.keys(s)) {\n if (takes.includes(key) || /^[$_]/.test(key)) continue;\n /* A key renamed within its own kind is told as the rename: \"only a view carries\" for a metric's `view` sent the\n author to the embed, when the card's destination is `to`. */\n const renamed = RENAMED_SECTION_KEYS[kind]?.[key];\n if (renamed) throw new Error(`${here} has \"${key}\", which is now ${renamed}.`);\n const owners = (Object.keys(SECTION_KEYS) as Array<AppDashSection['kind']>).filter((k) => SECTION_KEYS[k].includes(key));\n if (owners.length) throw new Error(`${here} has \"${key}\", which only a ${owners.map(sectionWord).join(' or ')} carries.`);\n throw new Error(`${here} (a ${sectionWord(kind)}) ${unknownKey(key, takes, keyHomesOf(key))}.`);\n }\n if (kind === 'custom') return parseCustomDashSection(s, here, data);\n if (kind === 'activity') return parseActivitySection(s, here, data);\n if (kind === 'find') return parseFindSection(s, here, data);\n /* The tenant's own Branding form: what it may change is the App's `branding` (checked at the root). The\n integrations' settings form is the same shape: what it shows is the jobs module's, read from the server. */\n if (kind === 'branding' || kind === 'integrations') {\n if (s.title !== undefined && (typeof s.title !== 'string' || !s.title.trim()))\n throw new Error(`${here} has a \"title\" that is not text.`);\n if (s.width !== undefined && !WIDTHS.includes(String(s.width) as SectionWidth))\n throw new Error(`${here} has a \"width\" of \"${String(s.width)}\". Use ${WIDTHS.join(', ')}.`);\n return {\n kind,\n ...(s.title === undefined ? {} : { title: (s.title as string).trim() }),\n ...(s.icon === undefined ? {} : { icon: parseIcon(s.icon, here) }),\n ...(s.width === undefined ? {} : { width: s.width as SectionWidth }),\n };\n }\n let banner: Pick<AppDashSection, 'variant' | 'eyebrow' | 'image'> = {};\n if (kind === 'note') {\n if (typeof s.text !== 'string' || !s.text.trim()) throw new Error(`${here} is a note and has no \"text\".`);\n if (s.action !== undefined) {\n parseAppLinkAction(s.action, `${here}, \"action\"`);\n }\n banner = parseNoteBanner(s, here, data);\n } else if (s.from === undefined ? !defaultTable : typeof s.from !== 'string' || !s.from.trim()) {\n throw new Error(\n `${here} has no \"from\"${\n defaultTable === undefined ? ' — name one, or set a dashboard-level \"from\" for every section to inherit' : ''\n }.`,\n );\n }\n if (typeof s.from === 'string') {\n checkResourceSlot(s.from.trim(), `${here}, \"from\"`, data);\n checkDeclaredResource(s.from.trim(), `${here}, \"from\"`, data);\n }\n if ((kind === 'breakdown' || kind === 'chart') && (typeof s.by !== 'string' || !s.by.trim())) {\n throw new Error(`${here} has no \"by\" — the column its groups are counted under.`);\n }\n let card: string[] | undefined;\n if (kind === 'board') {\n /* Lanes come from values of one of the table's own columns; a\n bucketed or dotted by has no value a card could sit under. */\n if (typeof s.by !== 'string' || !s.by.trim())\n throw new Error(`${here} is a board and has no \"by\" — the column whose values are its lanes.`);\n if (/[\\s.,]/.test(s.by.trim())) {\n throw new Error(\n `${here} has a \"by\" of \"${String(s.by).trim()}\" — a board's lanes come from a bare column of its own table, like \"status\".`,\n );\n }\n if (s.lanes !== undefined) {\n if (\n !Array.isArray(s.lanes) ||\n s.lanes.length < 1 ||\n s.lanes.length > 8 ||\n s.lanes.some((lane) => typeof lane !== 'string' || !lane.trim())\n ) {\n throw new Error(`${here} has \"lanes\" that are not a list of 1 to 8 nonblank strings.`);\n }\n if (new Set(s.lanes).size !== s.lanes.length)\n throw new Error(`${here} has duplicate \"lanes\". Each exact lane value may appear only once.`);\n }\n /* The one card vocabulary: a primary line, then its lines, label\n paths welcome. Absent, the card is the table's label column alone;\n lines beneath an unnamed primary would take its place. */\n if (s.primary !== undefined && (typeof s.primary !== 'string' || !s.primary.trim()))\n throw new Error(`${here} has a \"primary\" that is not a column or a label path.`);\n if (s.lines !== undefined && (!Array.isArray(s.lines) || s.lines.some((line) => typeof line !== 'string' || !line.trim()))) {\n throw new Error(`${here} has \"lines\" that are not a list of columns or label paths.`);\n }\n const lines = ((s.lines ?? []) as string[]).map((line) => line.trim());\n if (lines.length && s.primary === undefined) {\n throw new Error(\n `${here} has \"lines\" and no \"primary\": name the card's first line — the label column stands alone only on a card with no lines.`,\n );\n }\n if (s.primary !== undefined) card = [(s.primary as string).trim(), ...lines];\n }\n if (kind === 'matrix' && (typeof s.by !== 'string' || !s.by.trim() || !s.by.includes(','))) {\n throw new Error(\n `${here} is a matrix and needs a two-key \"by\" — \"stage, month close_date\": rows by the first, columns by the second.`,\n );\n }\n if (s.shape === 'funnel')\n throw new Error(\n `${here} has shape: 'funnel', which is now funnel(title, from, stages): ordered stages counted in one statement. Where rows stand now, by one column's values, is breakdown(title, from, column, { shape: 'steps' }).`,\n );\n const stages = kind === 'funnel' ? parseFunnelStages(s, here) : undefined;\n if (kind === 'breakdown') parseStepsShape(s, here);\n else if (s.shape !== undefined && !(DASH_CHART_SHAPES as readonly string[]).includes(String(s.shape))) {\n throw new Error(\n `${here} has a \"shape\" of \"${String(s.shape)}\". Use \"line\", \"bar\", \"donut\", \"stacked\", \"scatter\", \"heatmap\", \"waterfall\" or \"treemap\".`,\n );\n }\n if (kind === 'chart') parseChartShape(s, here);\n /* `auto` is a chart's word (autoBucket.ts): a breakdown lists its groups and a matrix crosses two fixed keys, and\n neither can have its unit move with the window; a heatmap's grid is a fixed grid of cells. */\n if (typeof s.by === 'string' && /(^|,)\\s*auto\\b/i.test(s.by)) {\n if (kind === 'matrix' || s.shape === 'heatmap') {\n throw new Error(\n `${here} is a ${kind === 'matrix' ? 'matrix' : 'heatmap'}, whose keys must both be fixed: \"auto\" picks a unit from the window, and a grid's cells cannot move with it. Write the unit, like \"month close_date\".`,\n );\n }\n if (kind !== 'chart') {\n throw new Error(\n `${here} has an \"auto\" key, which only a chart takes: its unit follows the window. Write the unit, like \"month close_date\".`,\n );\n }\n if (!autoKeyOf(s.by)) throw new Error(`${here} has a \"by\" of \"${s.by.trim()}\": \"auto\" cuts a date column, like \"auto created_at\".`);\n }\n if (s.shape === 'stacked' && (typeof s.by !== 'string' || !s.by.trim() || !s.by.includes(','))) {\n throw new Error(`${here} is a stacked chart and needs a two-key \"by\" — \"region, stage\": a bar for each region, divided by stage.`);\n }\n /* A goal a line or a bar can be measured against; a donut has no scale to rule it on. */\n /* A metric's goal may be read from data — a measure over a table and a query, in the metric's own words;\n a chart's rule is a number. */\n let target: number | DashTarget | undefined;\n /* A breakdown's goal is a row per group (G76): a seller's won against their quota, the target table's `by`\n matching each group's key, windowed by its own `over` as the section is by its. */\n const grouped = kind === 'breakdown';\n if (grouped && s.target !== undefined && (s.target === null || typeof s.target !== 'object' || Array.isArray(s.target)))\n throw new Error(\n `${here} has a \"target\" that is not a goal per group: { from, by, measure, over? }, a quota's amount by its owner.`,\n );\n if (grouped && s.target !== undefined && s.shape === 'steps')\n throw new Error(`${here} is drawn as steps, which have no bar to measure a \"target\" on.`);\n if (\n s.target !== undefined &&\n (kind === 'metric' || grouped) &&\n s.target !== null &&\n typeof s.target === 'object' &&\n !Array.isArray(s.target)\n ) {\n const t = s.target as Record<string, unknown>;\n const keys = grouped ? ['from', 'query', 'measure', 'by', 'over'] : ['from', 'query', 'measure'];\n if (t.table !== undefined)\n throw new Error(`${here} has a \"target\" with \"table\", which is now \"from\": the resource its goal is read from.`);\n const unknown = Object.keys(t).filter((key) => !keys.includes(key));\n if (unknown.length)\n throw new Error(\n grouped\n ? `${here} has a \"target\" with ${unknown.map((key) => `\"${key}\"`).join(', ')}. A breakdown's target says \"from\", \"by\", \"measure\", \"query\" and \"over\".`\n : `${here} has a \"target\" with ${unknown.map((key) => `\"${key}\"`).join(', ')}. A target from data says \"from\", \"query\" and \"measure\", as the metric does.`,\n );\n for (const key of keys) {\n if (t[key] !== undefined && (typeof t[key] !== 'string' || !(t[key] as string).trim()))\n throw new Error(`${here} has a \"target\" whose \"${key}\" is not a non-empty string.`);\n }\n if (grouped && (t.by === undefined || /\\s|,/.test((t.by as string).trim())))\n throw new Error(`${here} has a \"target\" with no single \"by\": the column of its table each group's key is matched on.`);\n if (grouped && typeof s.by === 'string' && /\\s|,/.test(s.by.trim()))\n throw new Error(`${here} groups by \"${s.by.trim()}\"; a target is matched on one plain column's value.`);\n if (t.query === undefined && t.measure === undefined && t.from === undefined)\n throw new Error(`${here} has an empty \"target\": give a number, or the \"from\", \"query\" and \"measure\" its goal is read from.`);\n if (t.from !== undefined) checkDeclaredResource((t.from as string).trim(), `${here}, \"target\", \"from\"`, data);\n if (typeof t.query === 'string') conditionsOnly(t.query, here, 'target');\n target = {\n table: (t.from as string | undefined)?.trim() ?? (typeof s.from === 'string' ? s.from.trim() : defaultTable) ?? '',\n ...(t.query === undefined ? {} : { query: (t.query as string).trim() }),\n measure: ((t.measure ?? s.measure ?? 'count') as string).trim(),\n ...(t.by === undefined ? {} : { by: (t.by as string).trim() }),\n ...(t.over === undefined ? {} : { over: (t.over as string).trim() }),\n };\n } else if (s.target !== undefined) {\n if (typeof s.target !== 'number' || !Number.isFinite(s.target)) {\n throw new Error(\n `${here} has a \"target\" that is not a number${kind === 'metric' ? ', or the \"from\", \"query\" and \"measure\" its goal is read from' : ''}.`,\n );\n }\n target = s.target;\n }\n if (s.curve !== undefined) {\n if (s.curve !== 'smooth' && s.curve !== 'straight')\n throw new Error(`${here} has a \"curve\" of ${JSON.stringify(s.curve)}. Use \"smooth\" or \"straight\".`);\n if (s.shape !== undefined && s.shape !== 'line')\n throw new Error(`${here} is a ${String(s.shape)} chart; only a line has a \"curve\".`);\n }\n if (s.total !== undefined) {\n if (s.shape !== 'donut') throw new Error(`${here} has a \"total\", which names a donut's middle figure; it is not a donut.`);\n if (typeof s.total !== 'string' || !s.total.trim() || s.total.trim().length > 40)\n throw new Error(`${here} has a \"total\" that is not a few words (at most 40 characters): what the middle figure counts.`);\n }\n if (s.target !== undefined && s.shape === 'donut')\n throw new Error(`${here} is a donut, which has no scale to draw a \"target\" on. Use a line or stacked bars.`);\n for (const key of ['title', 'query', 'measure', 'x', 'compare', 'by', 'sort', 'over'] as const) {\n if (s[key] !== undefined && (typeof s[key] !== 'string' || !(s[key] as string).trim())) {\n throw new Error(`${here} has a \"${key}\" that is not a non-empty string.`);\n }\n }\n for (const key of ['query', 'compare'] as const) {\n if (typeof s[key] === 'string' && s[key] !== PREVIOUS_PERIOD) conditionsOnly(s[key] as string, here, key);\n }\n const table = kind === 'note' ? undefined : typeof s.from === 'string' ? s.from.trim() : defaultTable;\n const columns = s.show === undefined ? undefined : parseShow(s.show, here);\n const labels = labelsFor(columns, table ? data.resources?.[table]?.labels : undefined);\n if (s.limit !== undefined && (typeof s.limit !== 'number' || !Number.isInteger(s.limit) || s.limit < 1)) {\n throw new Error(`${here} has a \"limit\" that is not a positive whole number.`);\n }\n if (s.height !== undefined && (typeof s.height !== 'number' || !Number.isFinite(s.height) || s.height < 120)) {\n throw new Error(`${here} has a \"height\" that is not a number of pixels (120 or more).`);\n }\n if (s.width !== undefined && !WIDTHS.includes(String(s.width) as SectionWidth)) {\n throw new Error(`${here} has a \"width\" of \"${String(s.width)}\". Use ${WIDTHS.join(', ')}.`);\n }\n if (s.better !== undefined) {\n if (!(BETTER as readonly string[]).includes(String(s.better)))\n throw new Error(`${here} has a \"better\" of \"${String(s.better)}\". Use \"up\" or \"down\".`);\n /* Without \"compare\" there is no change to colour: advice (appAdvisories), not a refusal. */\n }\n if (s.spark !== undefined && typeof s.spark !== 'boolean') throw new Error(`${here} has a \"spark\" that is not true or false.`);\n if (s.format !== undefined && !(FIGURE_FORMATS as readonly string[]).includes(String(s.format)))\n throw new Error(`${here} has a \"format\" of \"${String(s.format)}\". Use ${FIGURE_FORMAT_WORDS}.`);\n if (s.unit !== undefined && (typeof s.unit !== 'string' || !s.unit.trim() || s.unit.length > 24))\n throw new Error(`${here} has a \"unit\" that is not a short word said after the figure, like \"days\".`);\n if (s.figure !== undefined && s.figure !== 'exact' && s.figure !== 'compact')\n throw new Error(\n `${here} has a \"figure\" of ${JSON.stringify(s.figure)}. Use \"exact\" (to the cent, the default) or \"compact\" (\"$28.1K\").`,\n );\n const windows = parseSectionWindows(s, here);\n /* A metric counts its own query. The list it opens (`to:`) adds only its\n standing conditions — the rows that list can show at all, so the drill\n holds every row counted — never a tab: a tab is where a reader starts,\n not what the list is, and a metric with no query counted the first\n tab's open rows where it meant every row. The drill opens\n the list on exactly the counted rows (its \"All\" tab, where it has one).\n List plumbing is not part of a measure. Resolve this before windows and\n comparisons are derived, so every read and the drill share a cohort. */\n let opens: string | undefined;\n let query = s.query === undefined ? undefined : (s.query as string).trim();\n let compare = s.compare === undefined ? undefined : (s.compare as string).trim();\n /* A breakdown, chart or matrix says where its groups open the same way (`to: 'view-id'`, or `false` for groups that\n are text); a metric's card is the one doorway of its own. A group's list is judged as a metric's is. */\n const drills = kind === 'breakdown' || kind === 'chart' || kind === 'matrix' || kind === 'funnel';\n /* A metric's card takes false the same way: a figure read where it stands, opening nothing. */\n const doorway = drills || kind === 'metric';\n if (doorway && s.to !== undefined && s.to !== false && typeof s.to !== 'string')\n throw new Error(`${here} has a \"to\" that is not a list view's key or false.`);\n if ((kind === 'metric' || drills) && s.to !== undefined && s.to !== false) {\n const target = typeof s.to === 'string' ? views.get(s.to.trim()) : undefined;\n if (!target || target.kind !== 'list') {\n const lists = [...views.values()].filter((view) => view.kind === 'list').map((view) => view.key);\n throw new Error(\n `${here} opens \"${String(s.to)}\", which is not a list view this App declares. Its list views: ${lists.join(', ') || 'none'}.`,\n );\n }\n if (table !== undefined && target.view.table !== table) {\n throw new Error(\n `${here} counts ${table} and opens \"${target.key}\", a list of ${target.view.table}. ${drills ? 'A group opens a list of the rows it counted.' : 'A metric opens a list of the rows it counted.'}`,\n );\n }\n // An authored clause-only query must not become absent: parsing that\n // section again would silently count every row instead.\n for (const [key, value] of [\n ['query', query],\n ['compare', compare],\n ] as const) {\n if (value !== undefined && !splitClauseTail(value).head) {\n throw new Error(\n `${here} has a \"${key}\" with no filter conditions. A metric opening a list needs conditions, not only sort, show or limit clauses. ` +\n (key === 'query'\n ? 'Omit \"query\" to count every row the target view shows.'\n : 'Omit \"compare\" to leave out the comparison.'),\n );\n }\n }\n opens = target.key;\n query = metricCohortQuery(target.view.query, query);\n // A full compare is its own refinement; previous period is derived\n // later from the effective query.\n if (compare && compare !== PREVIOUS_PERIOD) compare = metricCohortQuery(target.view.query, compare);\n }\n /* The aside is a narrow column: rows there read as names, each with a\n quiet line under it and a value at its end — never a table's columns. */\n if (aside && kind === 'rows' && columns && columns.length > 3) {\n throw new Error(\n `${here} shows ${columns.length} columns. Rows in the aside draw as a name, a line under it and a value at its end: show at most three, or move them into \"sections\".`,\n );\n }\n /* What a section with nothing does: a rows section says so in place (guidance) or leaves itself out; the others\n leave themselves out. A dashboard has no 'collapse': the sections do not sit in a run of empty lists. */\n if (s.empty === false) throw new Error(`${here} has \"empty: false\", which is now \"empty: 'hide'\".`);\n if (s.empty === 'collapse')\n throw new Error(\n `${here} has \"empty: 'collapse'\", which only a record page's related lists take. A dashboard section that has nothing says so in place (rows take guidance) or is left out with \"empty: 'hide'\".`,\n );\n if (s.empty !== undefined && s.empty !== 'hide' && kind !== 'rows')\n throw new Error(\n `${here} (a ${sectionWord(kind)}) takes \"empty: 'hide'\", which leaves it out while it has nothing; guidance is a rows section's.`,\n );\n const emptyMode = s.empty === 'hide' ? ('hide' as const) : undefined;\n return {\n ...(opens === undefined ? {} : { view: opens }),\n ...(doorway && s.to === false ? { noDrill: true as const } : {}),\n kind: kind as AppDashSection['kind'],\n title: s.title === undefined ? undefined : String(s.title).trim(),\n table,\n text: s.text === undefined ? undefined : (s.text as string).trim(),\n /* A section's mark: beside its title, or a note's over its guidance. */\n ...(s.icon !== undefined ? { icon: parseIcon(s.icon, here) } : {}),\n ...(kind === 'note' && s.action !== undefined ? { action: parseAppLinkAction(s.action, `${here}, \"action\"`) } : {}),\n ...banner,\n query,\n measure: s.measure === undefined ? undefined : (s.measure as string).trim(),\n compare,\n better: s.better as AppDashSection['better'],\n spark: s.spark as boolean | undefined,\n ...(s.format === undefined ? {} : { format: s.format as AppDashSection['format'] }),\n ...(s.unit === undefined ? {} : { unit: (s.unit as string).trim() }),\n ...(s.figure === undefined ? {} : { figure: s.figure as 'exact' | 'compact' }),\n by: s.by === undefined ? undefined : (s.by as string).trim(),\n ...(stages ? { stages } : {}),\n ...(s.lanes === undefined ? {} : { lanes: [...(s.lanes as string[])] }),\n shape: s.shape as AppDashSection['shape'],\n ...(typeof s.x === 'string' ? { x: s.x.trim() } : {}),\n ...(s.curve === undefined ? {} : { curve: s.curve as 'smooth' | 'straight' }),\n ...(typeof s.total === 'string' ? { total: s.total.trim() } : {}),\n ...(s.notes === undefined\n ? {}\n : {\n notes: Object.fromEntries(Object.entries(s.notes as Record<string, string>).map(([k, v]) => [k, v.trim()])),\n }),\n over: s.over === undefined ? undefined : (s.over as string).trim(),\n ...(windows ? { windows } : {}),\n show: card ?? columns,\n ...(labels ? { labels } : {}),\n sort: s.sort === undefined ? undefined : (s.sort as string).trim(),\n ...(s.sizing === undefined ? {} : { sizing: parseTableSizing(s.sizing, here) }),\n ...(kind === 'rows' && s.empty !== undefined && !emptyMode ? { empty: parseListEmpty(s.empty, here) } : {}),\n ...(emptyMode ? { emptyMode } : {}),\n limit: s.limit as number | undefined,\n width: s.width as SectionWidth | undefined,\n ...(s.height === undefined ? {} : { height: s.height as number }),\n ...(target === undefined ? {} : { target }),\n };\n }),\n );\n /* Where a section stands on the layout is read beside its kind's keys, since every kind takes it the same way. */\n raw.forEach((entry, i) => {\n if (!isRecord(entry) || entry.area === undefined) return;\n gather(problems, () => {\n const here = `${at}, section ${i + 1}`;\n if (aside) throw new Error(`${here} has an \"area\", and the aside is not on the areas: an aside is a column of its own.`);\n if (entry.kind === 'firstRun')\n throw new Error(`${here} is a first run, which leads the dashboard outside its areas: it takes no \"area\".`);\n if (typeof entry.area !== 'string' || !isAreaName(entry.area.trim()))\n throw new Error(\n `${here} has an \"area\" that is not an area name: a lowercase word like \"trend\" (letters, digits and hyphens, at most 24).`,\n );\n if (entry.width !== undefined)\n throw new Error(`${here} has a \"width\" beside its \"area\": the area sets its width. Remove the \"width\".`);\n const section = sections[i];\n if (section) section.area = entry.area.trim();\n });\n });\n if (problems.length) throw new AppProblems(problems);\n return sections as AppDashSection[];\n}\n\n/**\n * A list view drawn inside a dashboard, read-only like any dashboard list:\n * the rows the view's room opens on (its standing query and a tab, or the\n * split's list query), drawn as its presentation reads — a grid or split as\n * a list of its show, a checklist or queue as a list of its card, a board as\n * a board. The dashboard's window and pick filters apply as to any section.\n */\nfunction embedSection(\n s: Record<string, unknown>,\n at: string,\n views: ReadonlyMap<string, ParsedView>,\n data: ParsedData,\n aside: boolean,\n): AppDashSection {\n allowKeys(s, VIEW_SECTION_KEYS, `${at} (a view)`);\n const listViews = [...views.values()].filter((view) => view.kind === 'list').map((view) => view.key);\n const parsed = typeof s.view === 'string' ? views.get(s.view) : undefined;\n if (!parsed || parsed.kind !== 'list') {\n throw new Error(\n `${at} embeds \"${String(s.view)}\", which is not a list view this App declares. Its list views: ${listViews.join(', ') || 'none'}.`,\n );\n }\n const { key, view } = parsed;\n const title = optionalText(s, 'title', at);\n const over = optionalText(s, 'over', at);\n if (s.width !== undefined && !WIDTHS.includes(String(s.width) as SectionWidth)) {\n throw new Error(`${at} has a \"width\" of \"${String(s.width)}\". Use ${WIDTHS.join(', ')}.`);\n }\n if (s.limit !== undefined && (typeof s.limit !== 'number' || !Number.isInteger(s.limit) || s.limit < 1)) {\n throw new Error(`${at} has a \"limit\" that is not a positive whole number.`);\n }\n let tab = view.tabs?.[0];\n if (s.tab !== undefined) {\n /* A tab is named by its address word, the one its URL carries (`?tab=`): a label is said in the reader's\n language and changes with `msg()`, the word does not. */\n tab = view.tabs?.find((candidate) => tabKey(candidate) === s.tab);\n if (!tab) {\n const byLabel = view.tabs?.find((candidate) => candidate.label === s.tab);\n throw new Error(\n byLabel\n ? `${at} names the tab \"${String(s.tab)}\" by its label. Name a tab by its address word: \"${tabKey(byLabel)}\".`\n : `${at} names the tab \"${String(s.tab)}\", which the view \"${key}\" does not have. ` +\n (view.tabs?.length ? `Its tabs: ${view.tabs.map((candidate) => tabKey(candidate)).join(', ')}.` : 'It has no tabs.'),\n );\n }\n }\n const presentation = view.presentation;\n const windows = parseSectionWindows(s, at);\n if (windows && presentation?.kind === 'board')\n throw new Error(`${at} embeds \"${key}\", a board, which takes the dashboard's window; its own \"windows\" are for a list.`);\n const interactive = embedInteractive(s, at, key, presentation?.kind, aside);\n /* What the embed says while it has nothing: its own guidance or 'hide', as a rows section takes; else the tab's,\n and on the first tab (the list as it opens) the list's own. A board says 'hide' or nothing: its lanes are its\n empty state. */\n if (s.empty === false) throw new Error(`${at} has \"empty: false\", which is now \"empty: 'hide'\".`);\n const own = s.empty === undefined || s.empty === 'hide' ? undefined : parseListEmpty(s.empty, at);\n if (own && presentation?.kind === 'board')\n throw new Error(`${at} embeds \"${key}\", a board, which takes \"empty: 'hide'\"; its lanes say it has nothing.`);\n const empty = own ?? tab?.empty ?? (tab === view.tabs?.[0] ? view.empty : undefined);\n const query = andQuery(view.query, tab?.query) || undefined;\n // The view's default order, where its tab says none of its own.\n const tabSorts = Boolean(tab?.query && /(^|\\s)(sort|order\\s+by)\\b/i.test(splitClauseTail(tab.query).tail));\n const base = {\n title: title ?? view.label ?? listLabel(view.table, data),\n table: view.table,\n ...(s.icon === undefined ? {} : { icon: parseIcon(s.icon, at) }),\n ...(query ? { query } : {}),\n ...(view.sort && !tabSorts ? { sort: view.sort } : {}),\n ...(s.width === undefined ? {} : { width: s.width as SectionWidth }),\n ...(s.limit === undefined ? {} : { limit: s.limit as number }),\n ...(over ? { over } : {}),\n ...(windows ? { windows } : {}),\n ...(interactive ? { interactive } : {}),\n ...(s.empty === 'hide' ? { emptyMode: 'hide' as const } : empty && presentation?.kind !== 'board' ? { empty } : {}),\n view: key,\n };\n if (!presentation || presentation.kind === 'split') {\n const show = view.show;\n const labels = labelsFor(show, data.resources?.[view.table]?.labels);\n return {\n kind: 'rows',\n ...base,\n ...(show ? { show } : {}),\n ...(labels ? { labels } : {}),\n ...(view.sizing === undefined ? {} : { sizing: view.sizing }),\n };\n }\n /* A custom list embedded is still its component's to draw (the rows section hands the view's presentation on). */\n if (presentation.kind === 'custom') return { kind: 'rows', ...base, show: [...presentation.show] };\n const card = [presentation.primary, ...('lines' in presentation ? (presentation.lines ?? []) : [])];\n if (presentation.kind === 'board') {\n if (s.width !== undefined) throw new Error(`${at} embeds \"${key}\", a board, which a dashboard draws full width. Remove \"width\".`);\n return {\n kind: 'board',\n ...base,\n by: presentation.by,\n ...(presentation.lanes ? { lanes: [...presentation.lanes] } : {}),\n show: card,\n };\n }\n return { kind: 'rows', ...base, show: card };\n}\n","/** What sdk/blocks says, in the custom components room (packages/kit/src/client/messages.ts says how the catalog is split). */\nimport { defineMessages } from '@mistriwork/kit/client/messages.js';\n\nexport const say = defineMessages({\n blockHasNo: '<{block}> has no \"{name}\". It takes {takes}.',\n blockRenamed: '<{block}> has \"{name}\", which is now \"{now}\".',\n blockNeeds: '<{block}> needs \"{name}\".',\n blockReadsUndeclared: '<{block}> reads \"{resource}\", which the page \"{label}\" does not read. It reads {reads}.',\n blockNotAResource: '<{block}> reads \"{resource}\", which is not one of this App\\'s resources.',\n blockValue: '<{block}> has a \"{name}\" of \"{value}\". Use {allowed}.',\n blockTabs: '<Tabs> needs \"tabs\": a list of tabs, each with its own \"key\", a \"label\" and its \"content\".',\n blockTabParam: '<Tabs> keeps its tab in \"{param}\", which is not one of the page\\'s params.',\n blockNoPage: 'This record has no page.',\n});\n","/**\n * The platform's blocks as a free-form page's components (`@mistriwork/mistri/page`): `<Rows>`, `<Metric>`, `<Chart>`,\n * `<Breakdown>`, `<RecordCard>`, `<Timeline>`, `<Tree>`, `<Gantt>`, `<Assistant>` and `<Tabs>`.\n *\n * Each is the component that draws the same thing on a dashboard, a record page or a list, handed the section it\n * would be handed there: no block draws anything of its own. Their props are the DSL's words for that section\n * (`metric('Open', 'ticket', { query, measure })` is `<Metric title=\"Open\" from=\"ticket\" query measure />`), so\n * there is one vocabulary, and each read goes through the same routes as the room's, under the reader's grants.\n *\n * This module is a part loaded when a page draws a block (page.tsx), and each block's own component is a part\n * loaded when that block is drawn: a page of metrics never fetches the Gantt.\n *\n * Each block's props are judged before it draws: a word it does not take, a value outside its words or a resource\n * the page does not read stands as the shared problem note in its place, saying which, marked as a part that could\n * not be drawn (an authoring mistake, which the browser suites fail on), while the rest of the page stays. What\n * throws while drawing stands behind the block's own boundary, as a dashboard section does.\n */\nimport { createElement, useContext, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';\nimport { pageFor, state } from '@mistriwork/kit/client/core.js';\nimport { tableNamed, type TableMeta } from '@mistriwork/engine/shared/schema.js';\nimport { parsePresentation } from '../../../shared/appViews.js';\nimport { BETTER, DASH_CHART_SHAPES } from '../../../shared/appModel.js';\nimport { FIGURE_FORMATS } from '@mistriwork/engine/shared/model.js';\nimport { TABLE_SIZINGS } from '@mistriwork/engine/shared/tableSizing.js';\nimport type { AppGanttList, AppTreeList } from '@mistriwork/engine/shared/listPresentations.js';\nimport { ErrorBoundary } from '@mistriwork/kit/client/components/ErrorBoundary.js';\nimport { Problem } from '@mistriwork/kit/client/components/Notes.js';\nimport { onDemand } from '@mistriwork/kit/client/components/onDemand.js';\nimport { captureRecordSurfaceOwner } from '@mistriwork/kit/client/components/recordSurface.js';\nimport type { Section } from '@mistriwork/kit/client/components/dashboardModel.js';\nimport type { PageSections } from '../RecordPageBody.js';\nimport type { PageEntry } from '@mistriwork/kit/client/components/pageModel.js';\nimport { UiPageContext, type UiPageFrame } from './context.js';\nimport { say } from '../../copy/components/components/sdk/blocks.js';\n\nconst DashboardBlock = onDemand(() => import('../Dashboard.js'), 'DashboardBlock');\nconst RecordPageBody = onDemand(() => import('../RecordPageBody.js'), 'RecordPageBody');\nconst TreeList = onDemand(() => import('../TreeList.js'), 'TreeList');\nconst GanttList = onDemand(() => import('../GanttList.js'), 'GanttList');\nconst AssistantRoom = onDemand(() => import('../AssistantPanel.js'), 'AssistantRoom');\nconst TabStrip = onDemand(() => import('@mistriwork/kit/client/components/PageSections.js'), 'TabStrip');\n\nconst WIDTHS = ['full', 'half', 'third', 'two-thirds'] as const;\nconst CURVES = ['smooth', 'straight'] as const;\nconst noop = () => {};\n\ntype Props = Readonly<Record<string, unknown>>;\n\n/* Words a block once took, refused with the one that replaced them. */\nconst RENAMED: Readonly<Record<string, string>> = { resource: 'from' };\n\n/* Each prop a block takes is one of its DSL words; anything else is a typo the author should hear about. */\nfunction takes(block: string, props: Props, words: readonly string[]): void {\n for (const name of Object.keys(props)) {\n if (words.includes(name)) continue;\n if (Object.hasOwn(RENAMED, name) && words.includes(RENAMED[name]))\n throw new Error(say('blockRenamed', { block, name, now: RENAMED[name] }));\n throw new Error(say('blockHasNo', { block, name, takes: words.join(', ') }));\n }\n}\n\nfunction needs(block: string, props: Props, name: string): string {\n const value = props[name];\n if (typeof value !== 'string' || !value.trim()) throw new Error(say('blockNeeds', { block, name }));\n return value.trim();\n}\n\nfunction oneOf<T extends string>(block: string, props: Props, name: string, allowed: readonly T[]): T | undefined {\n const value = props[name];\n if (value === undefined) return undefined;\n if (!allowed.includes(value as T)) {\n throw new Error(say('blockValue', { block, name, value: String(value), allowed: allowed.map((each) => `\"${each}\"`).join(', ') }));\n }\n return value as T;\n}\n\n/* The words a section carries as they are: text, a number, a list of text. The server's parser judges their sense. */\nfunction carried(props: Props, names: readonly string[]): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const name of names) if (props[name] !== undefined) out[name] = props[name];\n return out;\n}\n\n/** The table a block reads: one of the page's own reads, named as the App names it. */\nfunction resourceOf(block: string, resource: unknown, page: UiPageFrame | null): TableMeta {\n if (typeof resource !== 'string' || !resource) throw new Error(say('blockNeeds', { block, name: 'from' }));\n if (page && !page.reads.includes(resource)) {\n throw new Error(\n say('blockReadsUndeclared', { block, resource, label: page.label, reads: page.reads.map((name) => `\"${name}\"`).join(', ') }),\n );\n }\n const table = state.schema ? (tableNamed(state.schema, resource) as TableMeta | undefined) : undefined;\n if (!table) throw new Error(say('blockNotAResource', { block, resource }));\n return table;\n}\n\n/* A block: its props judged against the page it stands in, then drawn by the platform's component behind a boundary. */\nfunction block<R extends object>(judge: (props: Props, page: UiPageFrame | null) => R, draw: (resolved: R) => ReactNode) {\n return function Block(props: Props) {\n const page = useContext(UiPageContext);\n let resolved: R;\n try {\n resolved = judge(props, page);\n } catch (error) {\n const text = (error as Error).message;\n console.error(text);\n return <Problem crashed text={text} />;\n }\n return <ErrorBoundary>{createElement(draw as (props: R) => ReactNode, resolved)}</ErrorBoundary>;\n };\n}\n\n/* A dashboard's section, drawn where the page places it. Memoized on its words, so a page's redraw is not a refetch. */\nfunction SectionBlock({ section }: { section: Section }) {\n const words = JSON.stringify(section);\n const held = useMemo(() => section, [words]); // eslint-disable-line react-hooks/exhaustive-deps\n return <DashboardBlock section={held} />;\n}\n\nconst METRIC = ['title', 'from', 'query', 'measure', 'compare', 'better', 'format', 'unit', 'target', 'width'];\nfunction metricSection(props: Props, page: UiPageFrame | null): { section: Section } {\n takes('Metric', props, METRIC);\n const table = resourceOf('Metric', props.from, page);\n const title = needs('Metric', props, 'title');\n oneOf('Metric', props, 'better', BETTER);\n oneOf('Metric', props, 'format', FIGURE_FORMATS);\n oneOf('Metric', props, 'width', WIDTHS);\n /* A goal read from data is the metric's three words again; its table, where it names one, is one the page reads. */\n const goal = props.target;\n let target: Section['target'];\n if (goal !== null && typeof goal === 'object') {\n const asked = goal as { from?: string; table?: string; query?: string; measure?: string };\n if (asked.table !== undefined) throw new Error(say('blockRenamed', { block: 'Metric', name: 'target.table', now: 'target.from' }));\n target = {\n table: asked.from === undefined ? table.id : resourceOf('Metric', asked.from, page).id,\n ...(asked.query === undefined ? {} : { query: asked.query }),\n measure: asked.measure ?? (props.measure as string | undefined) ?? 'count',\n };\n } else if (goal !== undefined) target = goal as number;\n return {\n section: {\n kind: 'metric',\n table: table.id,\n ...carried(props, ['query', 'measure', 'compare', 'better', 'format', 'unit', 'width']),\n title,\n ...(target === undefined ? {} : { target }),\n } as Section,\n };\n}\n\n/* A chart's and a breakdown's figures are said as a metric's are: `format` and `unit` reach the dashboard's own sayer\n (dashboardModel's figureSayer), so a measure of seconds reads \"10h 2m\" on a bar, an axis and a tooltip. */\nconst CHART = [\n 'title',\n 'from',\n 'by',\n 'query',\n 'measure',\n 'shape',\n 'x',\n 'curve',\n 'total',\n 'target',\n 'limit',\n 'height',\n 'format',\n 'unit',\n 'width',\n];\nfunction chartSection(props: Props, page: UiPageFrame | null): { section: Section } {\n takes('Chart', props, CHART);\n const table = resourceOf('Chart', props.from, page);\n const title = needs('Chart', props, 'title');\n needs('Chart', props, 'by');\n oneOf('Chart', props, 'shape', DASH_CHART_SHAPES);\n oneOf('Chart', props, 'curve', CURVES);\n oneOf('Chart', props, 'format', FIGURE_FORMATS);\n oneOf('Chart', props, 'width', WIDTHS);\n return {\n section: {\n kind: 'chart',\n table: table.id,\n shape: 'line',\n ...carried(props, ['by', 'query', 'measure', 'shape', 'x', 'curve', 'total', 'target', 'limit', 'height', 'format', 'unit', 'width']),\n title,\n } as Section,\n };\n}\n\nconst BREAKDOWN = ['title', 'from', 'by', 'query', 'measure', 'limit', 'format', 'unit', 'width'];\nfunction breakdownSection(props: Props, page: UiPageFrame | null): { section: Section } {\n takes('Breakdown', props, BREAKDOWN);\n const table = resourceOf('Breakdown', props.from, page);\n const title = needs('Breakdown', props, 'title');\n needs('Breakdown', props, 'by');\n oneOf('Breakdown', props, 'format', FIGURE_FORMATS);\n oneOf('Breakdown', props, 'width', WIDTHS);\n return {\n section: {\n kind: 'breakdown',\n table: table.id,\n ...carried(props, ['by', 'query', 'measure', 'limit', 'format', 'unit', 'width']),\n title,\n } as Section,\n };\n}\n\nconst ROWS = ['title', 'from', 'query', 'show', 'sort', 'limit', 'sizing', 'width'];\nfunction rowsSection(props: Props, page: UiPageFrame | null): { section: Section } {\n takes('Rows', props, ROWS);\n const table = resourceOf('Rows', props.from, page);\n const title = needs('Rows', props, 'title');\n oneOf('Rows', props, 'sizing', TABLE_SIZINGS);\n oneOf('Rows', props, 'width', WIDTHS);\n const show = props.show as readonly string[] | undefined;\n return {\n section: {\n kind: 'rows',\n table: table.id,\n ...carried(props, ['query', 'sort', 'limit', 'sizing', 'width']),\n ...(show ? { show: [...show] } : {}),\n title,\n } as Section,\n };\n}\n\ninterface RecordAsk {\n table: TableMeta;\n recordKey: Record<string, unknown>;\n show?: readonly string[];\n only?: string;\n}\n\nfunction recordAsk(blockName: string, props: Props, page: UiPageFrame | null, only?: string): RecordAsk {\n const table = resourceOf(blockName, props.from, page);\n const record = props.record;\n if (!record || typeof record !== 'object' || Array.isArray(record))\n throw new Error(say('blockNeeds', { block: blockName, name: 'record' }));\n return {\n table,\n recordKey: record as Record<string, unknown>,\n ...(props.show === undefined ? {} : { show: props.show as readonly string[] }),\n ...(only ? { only } : {}),\n };\n}\n\n/* A record's own page, drawn in the page: its fields (or `show`'s) under its name and verbs, or only its timeline. */\nfunction RecordBlock({ table, recordKey, show, only }: RecordAsk) {\n const keyText = JSON.stringify(recordKey);\n const fields = show?.join('\\u0000');\n const mounted = useRef(true);\n useEffect(() => {\n mounted.current = true;\n return () => {\n mounted.current = false;\n };\n }, []);\n const owner = useMemo(() => captureRecordSurfaceOwner(() => mounted.current), [table.id, keyText]);\n const [pageId, setPageId] = useState<string | null>();\n useEffect(() => {\n let live = true;\n void (pageFor(table.id) as Promise<string | null>).then(\n (id) => live && setPageId(id ?? null),\n () => live && setPageId(null),\n );\n return () => {\n live = false;\n };\n }, [table.id]);\n const sections = useMemo(() => (show?.length ? ([{ kind: 'fields', paths: [...show] }] as PageSections) : undefined), [fields]); // eslint-disable-line react-hooks/exhaustive-deps\n if (pageId === null) return <p className=\"note\">{say('blockNoPage')}</p>;\n if (pageId === undefined) return null;\n return (\n <RecordPageBody\n key={keyText}\n owner={owner}\n tableId={table.id}\n recordKey={recordKey}\n pageId={pageId}\n close={noop}\n setPendingProbe={noop}\n onName={noop}\n onSettled={noop}\n {...(sections ? { sections } : {})}\n {...(only ? { only } : {})}\n />\n );\n}\n\n/* A tree and a Gantt chart are a list's presentations: their words are the presentation's, read by its own parser. */\ninterface Presented {\n table: TableMeta;\n spec: AppTreeList | AppGanttList;\n entry: { kind: 'table'; table: string; query: string };\n}\n\nfunction presented(blockName: string, kind: 'tree' | 'gantt', words: string[]) {\n return (props: Props, page: UiPageFrame | null): Presented => {\n takes(blockName, props, ['from', 'query', ...words]);\n const table = resourceOf(blockName, props.from, page);\n const spec = parsePresentation({ kind, ...carried(props, words) }, `<${blockName}>`) as AppTreeList | AppGanttList;\n return { table, spec, entry: { kind: 'table', table: table.id, query: (props.query as string | undefined) ?? '' } };\n };\n}\n\n/* Held on their words, so a page's redraw is not a refetch. */\nfunction TreeBlock({ table, spec, entry }: Presented) {\n const words = JSON.stringify([spec, entry]);\n const held = useMemo(() => ({ spec, entry }), [words]); // eslint-disable-line react-hooks/exhaustive-deps\n return <TreeList table={table} spec={held.spec as AppTreeList} entry={held.entry} />;\n}\n\nfunction GanttBlock({ table, spec, entry }: Presented) {\n const words = JSON.stringify([spec, entry]);\n const held = useMemo(() => ({ spec, entry }), [words]); // eslint-disable-line react-hooks/exhaustive-deps\n return <GanttList table={table} spec={held.spec as AppGanttList} entry={held.entry} />;\n}\n\ninterface TabSpec {\n key: string;\n label: ReactNode;\n content: ReactNode;\n}\n\ninterface TabsAsk {\n tabs: readonly TabSpec[];\n param?: string;\n label: string;\n page: UiPageFrame | null;\n}\n\nfunction tabsAsk(props: Props, page: UiPageFrame | null): TabsAsk {\n takes('Tabs', props, ['tabs', 'param', 'label']);\n const tabs = props.tabs as readonly TabSpec[] | undefined;\n if (\n !Array.isArray(tabs) ||\n !tabs.length ||\n tabs.some((tab) => !tab || typeof tab.key !== 'string' || !tab.key) ||\n new Set(tabs.map((tab) => tab.key)).size !== tabs.length\n ) {\n throw new Error(say('blockTabs'));\n }\n const param = props.param as string | undefined;\n if (param !== undefined && !page?.params.includes(param)) throw new Error(say('blockTabParam', { param: String(param) }));\n return { tabs, ...(param === undefined ? {} : { param }), label: typeof props.label === 'string' ? props.label : '', page };\n}\n\n/* Tabs, as a record page's are drawn: each panel drawn the first time it is opened, and kept in a page param if named. */\nfunction TabsBlock({ tabs, param, label, page }: TabsAsk) {\n const strip = tabs.map((tab) => ({ key: tab.key, token: tab.key, label: tab.label }));\n const entry = useMemo(() => ({ tab: param ? page?.get(param) : undefined }) as PageEntry, []); // eslint-disable-line react-hooks/exhaustive-deps\n return (\n <TabStrip\n tabs={strip}\n name={label}\n entry={entry}\n scoped={false}\n onTab={(token: string | undefined) => {\n if (param) page?.set(param, token);\n }}\n draw={(i: number) => tabs[i].content}\n />\n );\n}\n\nexport const Metric = block(metricSection, SectionBlock);\nexport const Chart = block(chartSection, SectionBlock);\nexport const Breakdown = block(breakdownSection, SectionBlock);\nexport const Rows = block(rowsSection, SectionBlock);\nexport const RecordCard = block((props, page) => {\n takes('RecordCard', props, ['from', 'record', 'show']);\n return recordAsk('RecordCard', props, page);\n}, RecordBlock);\nexport const Timeline = block((props, page) => {\n takes('Timeline', props, ['from', 'record']);\n return recordAsk('Timeline', props, page, 'timeline');\n}, RecordBlock);\nexport const Tree = block(presented('Tree', 'tree', ['parent', 'primary', 'lines']), TreeBlock);\nexport const Gantt = block(presented('Gantt', 'gantt', ['start', 'end', 'primary', 'by', 'progress']), GanttBlock);\nexport const Assistant = block(\n (props) => {\n takes('Assistant', props, ['conversation']);\n return typeof props.conversation === 'string' ? { conversation: props.conversation } : {};\n },\n (ask: { conversation?: string }) => <AssistantRoom {...ask} />,\n);\nexport const Tabs = block(tabsAsk, TabsBlock);\n"],"mappings":"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"}