@xano/sdk 1.0.0

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 (336) hide show
  1. package/CHANGELOG.md +788 -0
  2. package/LICENSE +21 -0
  3. package/README.md +541 -0
  4. package/dist/.build-fingerprint +1 -0
  5. package/dist/agent-file-refresh-ZEMK33AX.js +38 -0
  6. package/dist/answer-shape-K67TAAPY.js +33 -0
  7. package/dist/backend-dir-2U53CIAL.js +39 -0
  8. package/dist/backend-slot-7QXW2QYC.js +28 -0
  9. package/dist/bin.d.ts +1 -0
  10. package/dist/bin.js +255 -0
  11. package/dist/branch-NCRNWQBQ.js +61 -0
  12. package/dist/branch-commands-ACIRCEQJ.js +79 -0
  13. package/dist/bundle.d.ts +223 -0
  14. package/dist/bundle.js +153 -0
  15. package/dist/capture-WAXPD7DR.js +41 -0
  16. package/dist/chunk-27GWNP2I.js +23 -0
  17. package/dist/chunk-2CMJZDCL.js +177 -0
  18. package/dist/chunk-2EZ3ZJZZ.js +302 -0
  19. package/dist/chunk-2GNFDZA2.js +390 -0
  20. package/dist/chunk-2MNLRIOX.js +11 -0
  21. package/dist/chunk-2TFRR6JV.js +104 -0
  22. package/dist/chunk-2ZUEE4EK.js +408 -0
  23. package/dist/chunk-36JUQZVQ.js +22 -0
  24. package/dist/chunk-3DRXWLJ5.js +83 -0
  25. package/dist/chunk-3JDU4VRL.js +137 -0
  26. package/dist/chunk-3KZCLN4Z.js +241 -0
  27. package/dist/chunk-3LDULPRE.js +168 -0
  28. package/dist/chunk-3M4LFYNQ.js +111 -0
  29. package/dist/chunk-3XI5CTUE.js +22 -0
  30. package/dist/chunk-3YW23UFH.js +50 -0
  31. package/dist/chunk-45UIT5T2.js +862 -0
  32. package/dist/chunk-4BW2SG2N.js +46 -0
  33. package/dist/chunk-4KHEMC32.js +79 -0
  34. package/dist/chunk-4POWHT2K.js +321 -0
  35. package/dist/chunk-4Q2GN7NW.js +192 -0
  36. package/dist/chunk-5A2PLKZB.js +70 -0
  37. package/dist/chunk-5FBYXK5P.js +159 -0
  38. package/dist/chunk-5HVXPTU4.js +323 -0
  39. package/dist/chunk-5QU2P5CA.js +304 -0
  40. package/dist/chunk-5V5XU32D.js +191 -0
  41. package/dist/chunk-5ZSO4T2G.js +10 -0
  42. package/dist/chunk-622HUCZT.js +10 -0
  43. package/dist/chunk-65HWOMGV.js +177 -0
  44. package/dist/chunk-6ELFRN3P.js +365 -0
  45. package/dist/chunk-6HO3NCSJ.js +1055 -0
  46. package/dist/chunk-6IUMKYWI.js +457 -0
  47. package/dist/chunk-6KEB3C3U.js +65 -0
  48. package/dist/chunk-6MR5TI5V.js +987 -0
  49. package/dist/chunk-6NNTNXN7.js +157 -0
  50. package/dist/chunk-6PBR37LN.js +97 -0
  51. package/dist/chunk-6QXLMMT3.js +155 -0
  52. package/dist/chunk-6ZGJCBZS.js +224 -0
  53. package/dist/chunk-73IVBKMA.js +250 -0
  54. package/dist/chunk-7G3CTU7I.js +286 -0
  55. package/dist/chunk-A3UDOZB6.js +222 -0
  56. package/dist/chunk-A6E77GDW.js +85 -0
  57. package/dist/chunk-AHJWCYQO.js +660 -0
  58. package/dist/chunk-AKNCFVKZ.js +36 -0
  59. package/dist/chunk-AM4FKL4J.js +237 -0
  60. package/dist/chunk-AN6BL47T.js +26 -0
  61. package/dist/chunk-AOSYVMUS.js +147 -0
  62. package/dist/chunk-APWXY5SL.js +182 -0
  63. package/dist/chunk-AWEIIR2U.js +218 -0
  64. package/dist/chunk-AWQ7BQTQ.js +73 -0
  65. package/dist/chunk-BAUZOWGX.js +393 -0
  66. package/dist/chunk-BHACRS6R.js +940 -0
  67. package/dist/chunk-BWPNT55T.js +680 -0
  68. package/dist/chunk-C6RIMRU4.js +187 -0
  69. package/dist/chunk-CGVQZXYG.js +263 -0
  70. package/dist/chunk-CIMQZRRE.js +162 -0
  71. package/dist/chunk-CJHQKJU4.js +113 -0
  72. package/dist/chunk-CTMO46PK.js +1897 -0
  73. package/dist/chunk-D53J55C5.js +106 -0
  74. package/dist/chunk-D7VGG3WS.js +90 -0
  75. package/dist/chunk-DAD3ONIS.js +245 -0
  76. package/dist/chunk-DGWHAVRG.js +162 -0
  77. package/dist/chunk-DX4QR4B2.js +483 -0
  78. package/dist/chunk-EHP3WPEG.js +21 -0
  79. package/dist/chunk-ELP2HMXC.js +432 -0
  80. package/dist/chunk-ERVIASGL.js +21 -0
  81. package/dist/chunk-ET4Y4XCB.js +865 -0
  82. package/dist/chunk-ETO6LGLD.js +4750 -0
  83. package/dist/chunk-EUUUMY7J.js +24 -0
  84. package/dist/chunk-FQJ2TTHP.js +218 -0
  85. package/dist/chunk-FQOF7E2E.js +133 -0
  86. package/dist/chunk-FWD7AL6N.js +21 -0
  87. package/dist/chunk-FXFV3KU6.js +138 -0
  88. package/dist/chunk-FXRQOWTE.js +171 -0
  89. package/dist/chunk-G554NZNI.js +210 -0
  90. package/dist/chunk-GSGDWJB5.js +13 -0
  91. package/dist/chunk-H2V3N23D.js +66 -0
  92. package/dist/chunk-H5C7MBCR.js +159 -0
  93. package/dist/chunk-HHN734BU.js +371 -0
  94. package/dist/chunk-HRVBLCVJ.js +243 -0
  95. package/dist/chunk-HVRUQXGD.js +1264 -0
  96. package/dist/chunk-I4F5TCJH.js +130 -0
  97. package/dist/chunk-IDOTCMDV.js +57 -0
  98. package/dist/chunk-IGOLHKWF.js +427 -0
  99. package/dist/chunk-JGE7CAWB.js +148 -0
  100. package/dist/chunk-JTUKLCXS.js +92 -0
  101. package/dist/chunk-JZGVVJSI.js +66 -0
  102. package/dist/chunk-K4WX6UUU.js +923 -0
  103. package/dist/chunk-K5BBGTYZ.js +217 -0
  104. package/dist/chunk-K6RXBGUB.js +427 -0
  105. package/dist/chunk-KTSDSD36.js +165 -0
  106. package/dist/chunk-L5JYN3NM.js +1038 -0
  107. package/dist/chunk-LIEOKXKD.js +354 -0
  108. package/dist/chunk-LLNZGEFK.js +51 -0
  109. package/dist/chunk-LPECSJ2J.js +55 -0
  110. package/dist/chunk-LQCMKAJ7.js +413 -0
  111. package/dist/chunk-LRN4ZJCQ.js +1145 -0
  112. package/dist/chunk-MJ4ZVHKN.js +17 -0
  113. package/dist/chunk-MXHSQOC4.js +3061 -0
  114. package/dist/chunk-N5GKCWE5.js +367 -0
  115. package/dist/chunk-NA3PXMEF.js +10 -0
  116. package/dist/chunk-NAQDMEHV.js +76 -0
  117. package/dist/chunk-NWKXM5RD.js +109 -0
  118. package/dist/chunk-NZOWWHU5.js +196 -0
  119. package/dist/chunk-OHX6MIUZ.js +184 -0
  120. package/dist/chunk-OOXWYAA4.js +15 -0
  121. package/dist/chunk-OPHB5ORL.js +1990 -0
  122. package/dist/chunk-OXA6PSZT.js +1725 -0
  123. package/dist/chunk-P2HAGF6H.js +73 -0
  124. package/dist/chunk-PTHVMXZI.js +3036 -0
  125. package/dist/chunk-PZ5AY32C.js +10 -0
  126. package/dist/chunk-Q325JKYB.js +189 -0
  127. package/dist/chunk-Q7DGTQ5G.js +255 -0
  128. package/dist/chunk-QCNYHBL7.js +601 -0
  129. package/dist/chunk-QO6BVGS5.js +145 -0
  130. package/dist/chunk-QXTKSOFI.js +76 -0
  131. package/dist/chunk-ROY4YAEQ.js +66 -0
  132. package/dist/chunk-RTJRZFFY.js +447 -0
  133. package/dist/chunk-SAVJUS2X.js +128 -0
  134. package/dist/chunk-SFLHVCKX.js +173 -0
  135. package/dist/chunk-STSFNCY4.js +127 -0
  136. package/dist/chunk-SWBF2LTK.js +170 -0
  137. package/dist/chunk-T5KELPAR.js +109 -0
  138. package/dist/chunk-TFFGOFVZ.js +1481 -0
  139. package/dist/chunk-TLHIFK4M.js +256 -0
  140. package/dist/chunk-TLQE7ZN3.js +156 -0
  141. package/dist/chunk-TPHWJ7UG.js +1981 -0
  142. package/dist/chunk-TRJCALW3.js +291 -0
  143. package/dist/chunk-TTEIYD5P.js +133 -0
  144. package/dist/chunk-U64O2MXY.js +407 -0
  145. package/dist/chunk-UCQ2VUTG.js +630 -0
  146. package/dist/chunk-UTI5G6OJ.js +44 -0
  147. package/dist/chunk-UVHDMMIP.js +188 -0
  148. package/dist/chunk-UZ6QQWJT.js +120 -0
  149. package/dist/chunk-VAX3UZOY.js +686 -0
  150. package/dist/chunk-W3HBUKFO.js +57 -0
  151. package/dist/chunk-WKQLGKCL.js +2823 -0
  152. package/dist/chunk-WQFNASK4.js +560 -0
  153. package/dist/chunk-WRBKLYSJ.js +224 -0
  154. package/dist/chunk-WTPZRR4I.js +17 -0
  155. package/dist/chunk-WZDJ2LLX.js +2385 -0
  156. package/dist/chunk-X2NL4KUO.js +14 -0
  157. package/dist/chunk-X4DVXBFY.js +38 -0
  158. package/dist/chunk-X677DMZM.js +7050 -0
  159. package/dist/chunk-XBKMWY7F.js +612 -0
  160. package/dist/chunk-XNCP4KYO.js +78 -0
  161. package/dist/chunk-XVBGLISP.js +35 -0
  162. package/dist/chunk-XW6KISBB.js +1192 -0
  163. package/dist/chunk-YGZHHAAB.js +8156 -0
  164. package/dist/chunk-YHEFQUQB.js +186 -0
  165. package/dist/chunk-YLDALTWZ.js +653 -0
  166. package/dist/chunk-YU4HSQW7.js +229 -0
  167. package/dist/chunk-ZDPP6RKB.js +27 -0
  168. package/dist/chunk-ZFDZKNGG.js +194 -0
  169. package/dist/chunk-ZIY7RNUT.js +49 -0
  170. package/dist/chunk-ZYJP6BOO.js +30 -0
  171. package/dist/cli.d.ts +1111 -0
  172. package/dist/cli.js +165 -0
  173. package/dist/codegen-command-VR2GWR6C.js +173 -0
  174. package/dist/codegen.d.ts +115 -0
  175. package/dist/codegen.js +19 -0
  176. package/dist/commands-GJMJ4XYZ.js +76 -0
  177. package/dist/completion-ZQLO6PB6.js +738 -0
  178. package/dist/config-MBYGCAFK.js +136 -0
  179. package/dist/deploy-command-JVZPWHMK.js +180 -0
  180. package/dist/deploy-source-3W45CXMP.js +81 -0
  181. package/dist/dev-env-sync-HN44K4D5.js +188 -0
  182. package/dist/diagnostics-UEAAHWVF.js +30 -0
  183. package/dist/display-name-hint-ABBPVQIR.js +46 -0
  184. package/dist/emit-CyhBDxli.d.ts +7007 -0
  185. package/dist/env-example-refresh-ET5HMMDW.js +126 -0
  186. package/dist/env-pull-command-TNMNB3CV.js +323 -0
  187. package/dist/env-target-O4HRKTGZ.js +40 -0
  188. package/dist/env-var-command-ZIRDCAZA.js +427 -0
  189. package/dist/ephemeral-P7F3PMAD.js +54 -0
  190. package/dist/ephemeral-command-CLJJKOFT.js +592 -0
  191. package/dist/ephemeral-state-PYCBSOGX.js +61 -0
  192. package/dist/file-lock-PT74GWYW.js +10 -0
  193. package/dist/generate-command-AYEOLA26.js +510 -0
  194. package/dist/gitignore-3SJ3WNRG.js +51 -0
  195. package/dist/guards-4EBNCZDC.js +162 -0
  196. package/dist/hosted-file-PSHJB3HX.js +28 -0
  197. package/dist/impersonate-command-5JAS3WSV.js +152 -0
  198. package/dist/import-EQSFU6JC.js +19 -0
  199. package/dist/index.d.ts +5470 -0
  200. package/dist/index.js +371 -0
  201. package/dist/init-command-X2FR4UAK.js +125 -0
  202. package/dist/init-modules-RY7UHAOL.js +50 -0
  203. package/dist/init-web-M4YZSZK6.js +42 -0
  204. package/dist/internal.d.ts +179 -0
  205. package/dist/internal.js +2634 -0
  206. package/dist/io-RC56QSN5.js +26 -0
  207. package/dist/keep-data-merge-7MPLVAVI.js +1119 -0
  208. package/dist/lambda-config-reconcile-OUZ4OYAB.js +116 -0
  209. package/dist/landed-753YLJ5H.js +40 -0
  210. package/dist/landing-record-SU52SKXU.js +61 -0
  211. package/dist/live-diff-YONW2VI3.js +65 -0
  212. package/dist/local-engine-choice-CH5X3W7E.js +79 -0
  213. package/dist/local-engine-command-Z3OL7VOD.js +766 -0
  214. package/dist/local-engine-config-5U3SC434.js +39 -0
  215. package/dist/local-engine-deploy-PCQ4SNPB.js +39 -0
  216. package/dist/local-engine-process-EHTE5LLU.js +63 -0
  217. package/dist/local-engine-state-A4FTRFAF.js +32 -0
  218. package/dist/local-engine-update-check-6L645VBL.js +162 -0
  219. package/dist/lock-MRRHW5GS.js +87 -0
  220. package/dist/lock-commands-2QG25HL7.js +963 -0
  221. package/dist/login-command-QPYRWOXZ.js +538 -0
  222. package/dist/logout-command-WOIQLDP6.js +56 -0
  223. package/dist/loop-ALIZLOSX.js +191 -0
  224. package/dist/marketplace-command-K5YL42NO.js +977 -0
  225. package/dist/marketplace-resolve-LY32JES5.js +40 -0
  226. package/dist/meta-client-YXGPW53C.js +244 -0
  227. package/dist/module-manifest-JFGHU2TC.js +38 -0
  228. package/dist/near-key.d.ts +13 -0
  229. package/dist/near-key.js +8 -0
  230. package/dist/node.d.ts +95 -0
  231. package/dist/node.js +969 -0
  232. package/dist/operation-outcome-TZSESNC7.js +51 -0
  233. package/dist/output-target-AL3I44GG.js +47 -0
  234. package/dist/package-manager-PRAHUSCN.js +34 -0
  235. package/dist/plan-disclosure-6G4MLZKD.js +114 -0
  236. package/dist/plugin-contributions-WRNY5ABG.js +28 -0
  237. package/dist/plugin-prompt-4VA3B4FF.js +20 -0
  238. package/dist/plugin-questions-JTSYA74Q.js +24 -0
  239. package/dist/plugin.d.ts +396 -0
  240. package/dist/plugin.js +1 -0
  241. package/dist/preflight-command-2T5Z36MI.js +321 -0
  242. package/dist/profile-command-AH3PIM4X.js +800 -0
  243. package/dist/profile-pointer-GRWJU3YD.js +31 -0
  244. package/dist/profile-select-74TRDEPD.js +23 -0
  245. package/dist/project-dependency-KSH63ODT.js +35 -0
  246. package/dist/project-sdk-REPPOIBD.js +24 -0
  247. package/dist/promote-command-PCWT5N6S.js +140 -0
  248. package/dist/prompt-II3F3LND.js +27 -0
  249. package/dist/publish-command-TTDUUJI5.js +414 -0
  250. package/dist/pull-command-VBO7C4S3.js +161 -0
  251. package/dist/pull-superseded-GJWLG7XO.js +20 -0
  252. package/dist/release-command-N4ZPKMHR.js +146 -0
  253. package/dist/release-ns-command-3KFDMZ6G.js +1745 -0
  254. package/dist/response-HlXahv4K.d.ts +925 -0
  255. package/dist/retry-command-AS5EVREX.js +34 -0
  256. package/dist/routes-manifest-SNWUVBT6.js +43 -0
  257. package/dist/runtime-XETHVF33.js +14 -0
  258. package/dist/scaffold.d.ts +1911 -0
  259. package/dist/scaffold.js +107 -0
  260. package/dist/secrets-file-LVZD6IIM.js +56 -0
  261. package/dist/secrets-fill-command-TTCPSVXN.js +218 -0
  262. package/dist/seed-VU7C3QYS.js +46 -0
  263. package/dist/sent-writes-N7X4OVXT.js +22 -0
  264. package/dist/source-resolve-S2XA3XZF.js +83 -0
  265. package/dist/static-host-B2Y6AG3W.js +352 -0
  266. package/dist/status-command-RUDXTF7I.js +554 -0
  267. package/dist/store-6VNCD4D7.js +20 -0
  268. package/dist/store-ARM6VMO5.js +98 -0
  269. package/dist/store-DuYfzKlW.d.ts +3144 -0
  270. package/dist/suggest-6FOOYKHG.js +12 -0
  271. package/dist/sveltekit.d.ts +154 -0
  272. package/dist/sveltekit.js +109 -0
  273. package/dist/sync-contributions-UCEROO26.js +415 -0
  274. package/dist/table-ENAVMITR.js +42 -0
  275. package/dist/tables-command-GUPXXSB5.js +156 -0
  276. package/dist/tenant-IIHPOHGP.js +42 -0
  277. package/dist/tenant-command-D5HGI2X4.js +965 -0
  278. package/dist/test-command-TIKZMDPZ.js +476 -0
  279. package/dist/tests-BGGIOK3L.js +26 -0
  280. package/dist/token-NICJUTMT.js +72 -0
  281. package/dist/toolchain-hooks-GHKLUJOH.js +96 -0
  282. package/dist/toolchain-modules-5EAZUONB.js +24 -0
  283. package/dist/tracked-backend-EIBUMDZA.js +80 -0
  284. package/dist/update-check-2W2NLDTC.js +136 -0
  285. package/dist/upgrade-command-QHNFWM7O.js +531 -0
  286. package/dist/verify-rollout-DRCK5TDD.js +61 -0
  287. package/dist/whoami-command-STGINTXH.js +51 -0
  288. package/dist/workspace-2CLHU2LK.js +93 -0
  289. package/dist/workspace-binding-56PIIPCP.js +54 -0
  290. package/dist/workspace-command-JEXQETJD.js +535 -0
  291. package/dist/workspace-env-F2RFMSC5.js +70 -0
  292. package/dist/workspace-export-OJSH2HIW.js +21 -0
  293. package/dist/writable-VA7OISSK.js +25 -0
  294. package/dist/xano-ICFUSPFR.js +42 -0
  295. package/dist/xanosdk-import-GN42TO2B.js +56 -0
  296. package/dist/xanosdk-project-S6DXJONQ.js +41 -0
  297. package/dist/xdo-CxDr2Q1C.d.ts +1371 -0
  298. package/guides/README.md +33 -0
  299. package/guides/authoring.md +921 -0
  300. package/guides/cli.md +401 -0
  301. package/guides/codegen.md +177 -0
  302. package/guides/codes.md +404 -0
  303. package/guides/coverage.md +74 -0
  304. package/guides/deploying.md +1525 -0
  305. package/guides/environment.md +400 -0
  306. package/guides/marketplace.md +415 -0
  307. package/guides/module-contract.md +558 -0
  308. package/guides/object-kinds.md +433 -0
  309. package/guides/project-structure.md +43 -0
  310. package/guides/scaffold.md +351 -0
  311. package/guides/typed-frontend.md +242 -0
  312. package/llms/client.md +63 -0
  313. package/llms/errors.md +56 -0
  314. package/llms/fields.md +176 -0
  315. package/llms/filters.md +183 -0
  316. package/llms/kinds-agent-mcp.md +30 -0
  317. package/llms/kinds-core.md +100 -0
  318. package/llms/kinds-knowledge.md +20 -0
  319. package/llms/kinds-realtime.md +76 -0
  320. package/llms/lambda.md +94 -0
  321. package/llms/legacy.md +44 -0
  322. package/llms/lock.md +40 -0
  323. package/llms/object-kinds.md +34 -0
  324. package/llms/statements-calls.md +62 -0
  325. package/llms/statements-catalog.md +320 -0
  326. package/llms/statements-data.md +66 -0
  327. package/llms/statements-runtime.md +68 -0
  328. package/llms/tests.md +59 -0
  329. package/llms/toolchain.md +16 -0
  330. package/llms/triggers.md +36 -0
  331. package/llms/values.md +29 -0
  332. package/llms-full.txt +2085 -0
  333. package/llms.txt +493 -0
  334. package/manifest.json +11224 -0
  335. package/package.json +162 -0
  336. package/types/lambda-globals.d.ts +129 -0
@@ -0,0 +1,2634 @@
1
+ import {
2
+ SUPERSEDED_STATEMENTS
3
+ } from "./chunk-27GWNP2I.js";
4
+ import {
5
+ FIELD_METHODS
6
+ } from "./chunk-3YW23UFH.js";
7
+ import {
8
+ CAPABILITIES,
9
+ STATEMENT_CAPABILITIES,
10
+ bundleCapabilities
11
+ } from "./chunk-JGE7CAWB.js";
12
+ import {
13
+ COMMANDS,
14
+ FLAGS,
15
+ GLOBAL_FLAGS,
16
+ flagSpec,
17
+ flagSummary,
18
+ publishedFlagValues,
19
+ selectorValues,
20
+ sourceSpellings,
21
+ visibleFlags
22
+ } from "./chunk-PTHVMXZI.js";
23
+ import "./chunk-MJ4ZVHKN.js";
24
+ import {
25
+ GENERATED_STATEMENT_NAMES,
26
+ HEADER_DUPLICATES,
27
+ KNOWLEDGE_DIR,
28
+ KNOWLEDGE_FILE,
29
+ STORAGE_ACCESS,
30
+ addonKind,
31
+ agentKind,
32
+ channelPathParams,
33
+ encodeAddon,
34
+ encodeAgent,
35
+ encodeFunction,
36
+ encodeKnowledge,
37
+ encodeMcpServer,
38
+ encodeMiddleware,
39
+ encodeRealtimeChannel,
40
+ encodeRealtimeMessage,
41
+ encodeRealtimeServer,
42
+ encodeSchedule,
43
+ encodeTask,
44
+ encodeTool,
45
+ encodeToolRefs,
46
+ encodeToolsetBase,
47
+ encodeTrigger,
48
+ encodeWorkflowTest,
49
+ functionKind,
50
+ isKnowledgeDirSource,
51
+ isKnowledgeFileSource,
52
+ knowledgeKind,
53
+ mcpServerKind,
54
+ middlewareKind,
55
+ realtimeChannelGuid,
56
+ realtimeChannelKind,
57
+ realtimeMessageGuid,
58
+ realtimeMessageKind,
59
+ realtimeServerKind,
60
+ resolveRealtimeServerCanonical,
61
+ resolveToolsetCanonical,
62
+ serializeBundle,
63
+ taskKind,
64
+ toolKind,
65
+ triggerKind,
66
+ workflowTestKind
67
+ } from "./chunk-MXHSQOC4.js";
68
+ import {
69
+ encodeQuery,
70
+ encodeResponse,
71
+ queryKind
72
+ } from "./chunk-6IUMKYWI.js";
73
+ import {
74
+ encodeWorkspaceConfig,
75
+ workspaceKind
76
+ } from "./chunk-2CMJZDCL.js";
77
+ import "./chunk-CTMO46PK.js";
78
+ import {
79
+ resolveAuthRef
80
+ } from "./chunk-ZDPP6RKB.js";
81
+ import "./chunk-K5BBGTYZ.js";
82
+ import {
83
+ PAYLOAD_ARRAY_KEYS,
84
+ buildBundle,
85
+ calcSignatureJson,
86
+ phpJsonEncode
87
+ } from "./chunk-LQCMKAJ7.js";
88
+ import {
89
+ DECODE_ONLY_STATEMENTS,
90
+ GENERATED_SPECS,
91
+ PRECONDITION_ERROR_STATUS,
92
+ PRECONDITION_ERROR_TYPES,
93
+ QUERY_EXPRESSION_FILTERS,
94
+ VECTOR_FILTERS,
95
+ apiGroupKind,
96
+ declaredServicePorts,
97
+ encodeApiGroup,
98
+ encodeFromSpec,
99
+ encodeMicroservice,
100
+ generated,
101
+ isQueryExpressionFilter,
102
+ microserviceKind,
103
+ registerSpec
104
+ } from "./chunk-YGZHHAAB.js";
105
+ import "./chunk-XVBGLISP.js";
106
+ import {
107
+ FILTER_NAMES,
108
+ FILTER_SPECS
109
+ } from "./chunk-6HO3NCSJ.js";
110
+ import {
111
+ buildMiddlewareBlock,
112
+ buildWorkspaceHistory,
113
+ encodeContainerHistory,
114
+ encodeHistory,
115
+ encodeMiddlewareEntry,
116
+ encodeMiddlewareList
117
+ } from "./chunk-5V5XU32D.js";
118
+ import "./chunk-GSGDWJB5.js";
119
+ import {
120
+ encodeColumn,
121
+ encodeIndex,
122
+ encodeTable,
123
+ encodeView,
124
+ tableKind
125
+ } from "./chunk-HVRUQXGD.js";
126
+ import "./chunk-ERVIASGL.js";
127
+ import {
128
+ encodeObject,
129
+ getKind,
130
+ isRegisteredKind,
131
+ registerKind,
132
+ registeredKinds
133
+ } from "./chunk-4POWHT2K.js";
134
+ import "./chunk-H2V3N23D.js";
135
+ import "./chunk-3JDU4VRL.js";
136
+ import "./chunk-WTPZRR4I.js";
137
+ import "./chunk-UTI5G6OJ.js";
138
+ import "./chunk-2EZ3ZJZZ.js";
139
+ import {
140
+ LOCK_PAYLOAD_KEYS,
141
+ LOCK_VERSION,
142
+ WORKSPACE_KEY,
143
+ adoptFromBundle,
144
+ createLockContext,
145
+ emptyLock,
146
+ lockKey,
147
+ mergeObserved,
148
+ mintCanonical,
149
+ parseLock,
150
+ recordObserved,
151
+ renameLockEntry,
152
+ resolvePayloadKey,
153
+ serializeLock,
154
+ validateLockModel
155
+ } from "./chunk-6MR5TI5V.js";
156
+ import "./chunk-5ZSO4T2G.js";
157
+ import "./chunk-XNCP4KYO.js";
158
+ import "./chunk-AKNCFVKZ.js";
159
+ import {
160
+ encodeStatement,
161
+ getStatementFactory,
162
+ isRegisteredStatement
163
+ } from "./chunk-YU4HSQW7.js";
164
+ import "./chunk-TFFGOFVZ.js";
165
+ import {
166
+ IMPLEMENTED_STATEMENTS,
167
+ STATEMENT_SURFACES,
168
+ TOTAL_STATEMENTS,
169
+ sPathOf
170
+ } from "./chunk-Q7DGTQ5G.js";
171
+ import "./chunk-G554NZNI.js";
172
+ import {
173
+ toNestedFields
174
+ } from "./chunk-N5GKCWE5.js";
175
+ import {
176
+ LAMBDA_BINDINGS,
177
+ LAMBDA_GLOBALS,
178
+ LAMBDA_MODULE_GLOBALS,
179
+ TAGS
180
+ } from "./chunk-OXA6PSZT.js";
181
+ import "./chunk-VAX3UZOY.js";
182
+ import {
183
+ REFERENCEABLE_KINDS,
184
+ deriveGuid,
185
+ resolveRef
186
+ } from "./chunk-NZOWWHU5.js";
187
+ import {
188
+ getLockedCanonical,
189
+ getLockedGuid,
190
+ isLockSeeded
191
+ } from "./chunk-4BW2SG2N.js";
192
+ import "./chunk-TTEIYD5P.js";
193
+ import "./chunk-CIMQZRRE.js";
194
+ import "./chunk-OHX6MIUZ.js";
195
+ import "./chunk-36JUQZVQ.js";
196
+ import "./chunk-PZ5AY32C.js";
197
+
198
+ // src/statements/byte-evidence.ts
199
+ var UNCONFIRMED_STATEMENTS = /* @__PURE__ */ new Set([]);
200
+ var UNCONFIRMED_NOTE = "Built from the engine's schema declaration, with no stored instance behind them to compare the emitted bytes against \u2014 every other statement has one. Fully authorable and not deprecated; the field schema may simply be less exact than the rest. If a deploy rejects one of these, read it as a shape question and pull the workspace back to see what the engine stored, rather than rewriting working code around it.";
201
+
202
+ // src/manifest/manifest.ts
203
+ function argGrammar(args) {
204
+ if (args === void 0 || args.length === 0) return void 0;
205
+ return args.map((a) => a.required ? `<${a.name}>` : `[${a.name}]`).join(" ");
206
+ }
207
+ function flagsOf(refs) {
208
+ if (refs === void 0 || refs.length === 0) return void 0;
209
+ return refs.map((ref) => ({ flag: flagSpec(ref), description: flagSummary(ref), ...publishedFlagValues(ref) }));
210
+ }
211
+ function selectorsOf(args, refs) {
212
+ const slots = [
213
+ ...(args ?? []).flatMap((a) => a.selector ? [[`<${a.name}>`, a.selector]] : []),
214
+ ...(refs ?? []).flatMap(
215
+ (r) => typeof r !== "string" && r.selector ? [[`--${r.key}`, r.selector]] : []
216
+ )
217
+ ];
218
+ if (slots.length === 0) return void 0;
219
+ return slots.map(([slot, sel]) => ({
220
+ slot,
221
+ role: sel.role,
222
+ default: sel.default,
223
+ grammar: sourceSpellings(sel.accepted),
224
+ values: selectorValues(sel),
225
+ ...sel.refused !== void 0 ? { refused: { ...sel.refused } } : {}
226
+ }));
227
+ }
228
+ function buildCliGlobalFlags() {
229
+ return GLOBAL_FLAGS.map((key) => ({
230
+ flag: FLAGS[key].spec,
231
+ description: FLAGS[key].summary
232
+ }));
233
+ }
234
+ function buildCli() {
235
+ const out = [];
236
+ for (const [name, spec] of Object.entries(COMMANDS)) {
237
+ const description = spec.aliasOf === void 0 ? spec.summary : `${spec.summary} (alias of \`${spec.aliasOf}\`)`;
238
+ const subs = Object.entries(spec.subcommands ?? {});
239
+ if (subs.length === 0) {
240
+ out.push({
241
+ command: name,
242
+ ...argGrammar(spec.args) !== void 0 ? { args: argGrammar(spec.args) } : {},
243
+ ...flagsOf(visibleFlags(spec)) !== void 0 ? { flags: flagsOf(visibleFlags(spec)) } : {},
244
+ ...selectorsOf(spec.args, visibleFlags(spec)) !== void 0 ? { selectors: selectorsOf(spec.args, visibleFlags(spec)) } : {},
245
+ description
246
+ });
247
+ continue;
248
+ }
249
+ for (const [subName, sub] of subs) {
250
+ if (sub.unreleased !== void 0) continue;
251
+ const args = argGrammar(sub.args ?? spec.args);
252
+ const refs = sub.flags !== void 0 ? visibleFlags(sub) : visibleFlags(spec);
253
+ const flags = flagsOf(refs);
254
+ const selectors = selectorsOf(sub.args ?? spec.args, refs);
255
+ out.push({
256
+ command: `${name} ${subName}`,
257
+ ...args !== void 0 ? { args } : {},
258
+ ...flags !== void 0 ? { flags } : {},
259
+ ...selectors !== void 0 ? { selectors } : {},
260
+ description: sub.summary
261
+ });
262
+ }
263
+ }
264
+ return out.sort((a, b) => a.command.localeCompare(b.command));
265
+ }
266
+ var KIND_DESCRIPTORS = [
267
+ { kind: "function", payloadKey: "function", authorFactory: "defineFunction", description: "Reusable server-side logic (a custom function) callable from any stack via `s.function.run`.", registerMethod: "registerFunctions" },
268
+ { kind: "table", payloadKey: "dbo", authorFactory: "table", description: "A database table: typed columns (`f.*`), indexes, and views; the schema other kinds read and write.", registerMethod: "registerTables" },
269
+ { kind: "query", payloadKey: "query", authorFactory: "query", description: "An HTTP API endpoint (verb + path) bound to an API group; the main request/response surface.", registerMethod: "registerQueries" },
270
+ { kind: "api_group", payloadKey: "app", authorFactory: "apiGroup", description: "A container that groups queries under a shared base path, CORS, and swagger config.", registerMethod: "registerApiGroups" },
271
+ {
272
+ kind: "trigger",
273
+ payloadKey: "trigger",
274
+ authorFactory: "{tableTrigger,realtimeServerTrigger,realtimeChannelTrigger,mcpServerTrigger,agentTrigger,workspaceTrigger,errorTrigger}",
275
+ description: "An event-driven handler fired by a DB write, a realtime server connection or channel join/leave/deliver, an MCP/agent connection, a branch lifecycle event, or an error \u2014 inputs are implied by type and arrive on the `t` handle.",
276
+ registerMethod: "registerTriggers",
277
+ subKinds: [
278
+ { authorFactory: "tableTrigger", objType: "database", description: 'Fires when rows change on a bound table (insert/update/delete/truncate). The changed row is exposed as `t.new`/`t.old`, typed to the table when a `table()` handle is bound. Config-only (no response). `search` filters rows in the DATABASE, so it uses `col("NEW.x")`/`col("OLD.x")`, not `t`; invalid with `truncate`, and insert/delete cannot read the absent side.' },
279
+ { authorFactory: "realtimeTrigger", objType: "workspace_realtime_channel", legacy: true, description: "the SUPERSEDED realtime trigger, against the workspace-global realtime layer \u2014 a different object from the current `channel`, despite the similar name. For a join hook use `realtimeChannelTrigger({ actions: { join: true } })`; for message handling use a `realtimeMessage()` handler, which is the current equivalent of its `message` action (a message is an authored unit now, not a trigger action)." },
280
+ { authorFactory: "realtimeServerTrigger", objType: "realtime_server", description: "Fires when a client connects to or disconnects from a realtime server; inspect the connecting client and its permissions via `t`. Bind with `realtimeServer`. Response-bearing." },
281
+ { authorFactory: "realtimeChannelTrigger", objType: "channel", description: "Fires when a client joins or leaves a channel; inspect the addressed channel path and the client via `t`. Bind with a `realtimeChannel()` handle (a bare path is ambiguous across servers). Response-bearing." },
282
+ { authorFactory: "mcpServerTrigger", objType: "toolset", description: "Fires when an MCP client connects to a bound MCP server; filter its tools/prompts/resources via `t`. Response-bearing." },
283
+ { authorFactory: "agentTrigger", objType: "toolset", description: "Fires when a client connects to a bound agent; gate or annotate its toolset via `t.toolset`/`t.tools`. Response-bearing." },
284
+ { authorFactory: "workspaceTrigger", objType: "workspace", description: "Fires on branch lifecycle events (branch new/merge/live); inspect the from/to branch and action via `t`. Config-only." },
285
+ { authorFactory: "errorTrigger", objType: "error", description: "Fires when an error signature is first seen, regresses, or is marked fixed; inspect the error, caller, statement, and occurrence counts via `t`. Config-only." }
286
+ ]
287
+ },
288
+ { kind: "tool", payloadKey: "tool", authorFactory: "tool", description: "An agent/MCP tool: a callable capability with typed inputs an AI agent can invoke.", registerMethod: "registerTools" },
289
+ { kind: "prompt", payloadKey: "prompt", authorFactory: "prompt", description: "An MCP prompt: typed arguments plus a stack that builds the messages an MCP client receives. Exposed by an `mcpServer`.", registerMethod: "registerPrompts" },
290
+ { kind: "resource", payloadKey: "resource", authorFactory: "resource", description: "An MCP resource: contents an MCP client reads by URI (a literal or a `{variable}` template), produced by a stack. Exposed by an `mcpServer`.", registerMethod: "registerResources" },
291
+ { kind: "mcp_server", payloadKey: "toolset", authorFactory: "mcpServer", description: "An MCP server exposing tools, prompts and resources to external MCP clients.", registerMethod: "registerMcpServers" },
292
+ { kind: "agent", payloadKey: "toolset", authorFactory: "agent", description: "An AI agent: an LLM configuration plus the tools it can call. Invoke it from any stack (query/function/task/tool/trigger) with `s.ai.agent.run` \u2014 no public endpoint; the result is a rich envelope whose completion text is at `.result`.", registerMethod: "registerAgents" },
293
+ { kind: "task", payloadKey: "task", authorFactory: "task", description: "A scheduled background job (cron/interval) that runs a stack on a timer.", registerMethod: "registerTasks" },
294
+ { kind: "workflow_test", payloadKey: "workflow_test", authorFactory: "workflowTest", description: 'An end-to-end test: a named stack with NO input and NO response that invokes other objects (`s.function.call`, `s.task.call`, `s.api.call`) and asserts on what they bind with `s.expect.*`. `datasource` defaults to `""` (an EMPTY datasource, recommended); naming one CLONES it before every run \u2014 see `llms/kinds-core.md`.', registerMethod: "registerWorkflowTests" },
295
+ { kind: "middleware", payloadKey: "middleware", authorFactory: "middleware", description: "A reusable pre/post stack attached to a query/function/task/tool/API group to run before or after its own logic.", registerMethod: "registerMiddleware" },
296
+ { kind: "addon", payloadKey: "addon", authorFactory: "addon", description: "A reusable read fragment that enriches a query result by joining related table data.", registerMethod: "registerAddons" },
297
+ { kind: "realtime_server", payloadKey: "realtime_server", authorFactory: "realtimeServer", description: "A realtime (websocket) server: the canonical-addressed container that owns realtime channels. Off until `enabled: true`. Returns a handle with `getUrl(baseUrl)`/`getPath()` for the client's socket URL (`wss://<host>/ws/<canonical>`).", registerMethod: "registerRealtimeServers" },
298
+ { kind: "channel", payloadKey: "channel", authorFactory: "realtimeChannel", description: "A realtime channel: a joinable path on a realtime server (`rooms/{room_id}`) with typed path params, join/publish policy, a client-visible conversation transcript, and delivery semantics. Owns message handlers. Returns a handle with `getChannel(params)` for the path a client joins.", registerMethod: "registerRealtimeChannels" },
299
+ { kind: "message", payloadKey: "message", authorFactory: "realtimeMessage", description: "A realtime message handler: a named message type on a channel with its own typed payload and stack \u2014 the realtime analogue of a query. Pass the `realtimeChannel()` handle as `channel` and the owning server comes with it.", registerMethod: "registerRealtimeMessages" },
300
+ { kind: "microservice", payloadKey: "microservice", authorFactory: "microservice", description: "A container workload deployed alongside the workspace, called from a stack with `s.microservice.request`. Def shapes (`builtin` containers vs a `helm` chart) and the SECRET-handling rules its config carries: `llms/statements-calls.md`.", registerMethod: "registerMicroservices" },
301
+ {
302
+ kind: "knowledge",
303
+ payloadKey: "knowledge",
304
+ authorFactory: "knowledge",
305
+ description: 'Markdown Xano\'s own AI (the builder agent, and Meta API / MCP readers) reads before it acts \u2014 never an `agent()` run \u2014 an `agents.md` of standing instructions, a `skill`, or a `doc`. The body is a real markdown FILE via `knowledgeFile("./x.md", import.meta.url)`, and `mode` decides how much of it reaches the agent.',
306
+ registerMethod: "registerKnowledge"
307
+ },
308
+ { kind: "workspace", payloadKey: "workspace", authorFactory: "workspaceConfig", description: "Workspace-level configuration such as default middleware chains and request-history defaults per host kind.", registerMethod: "registerWorkspace" }
309
+ ];
310
+ var ENGINE_OBJECT_KINDS = [
311
+ { kind: "addon", authorFactory: "addon" },
312
+ { kind: "agent", authorFactory: "agent" },
313
+ { kind: "agent_trigger", authorFactory: "agentTrigger" },
314
+ { kind: "api_group", authorFactory: "apiGroup" },
315
+ { kind: "branch", authorFactory: null, absence: "instance-owned: a branch is instance state, not workspace source" },
316
+ { kind: "channel", authorFactory: "realtimeChannel" },
317
+ { kind: "channel_trigger", authorFactory: "realtimeChannelTrigger" },
318
+ { kind: "error_trigger", authorFactory: "errorTrigger" },
319
+ { kind: "function", authorFactory: "defineFunction" },
320
+ { kind: "market_item", authorFactory: null, absence: "instance-owned: marketplace provenance belongs to the instance that installed it" },
321
+ { kind: "mcp_server", authorFactory: "mcpServer" },
322
+ { kind: "mcp_server_trigger", authorFactory: "mcpServerTrigger" },
323
+ { kind: "message", authorFactory: "realtimeMessage" },
324
+ { kind: "microservice", authorFactory: "microservice" },
325
+ { kind: "knowledge", authorFactory: "knowledge" },
326
+ { kind: "middleware", authorFactory: "middleware" },
327
+ { kind: "prompt", authorFactory: "prompt" },
328
+ { kind: "query", authorFactory: "query" },
329
+ {
330
+ kind: "realtime_channel",
331
+ authorFactory: null,
332
+ absence: "unmodeled: the SUPERSEDED workspace-global realtime channel. Its trigger is authorable (`realtimeTrigger`) so a legacy workspace's handlers survive a pull, but the channel object itself is not \u2014 author a `realtimeServer` + `realtimeChannel` instead"
333
+ },
334
+ { kind: "realtime_server", authorFactory: "realtimeServer" },
335
+ { kind: "realtime_server_trigger", authorFactory: "realtimeServerTrigger" },
336
+ { kind: "realtime_trigger", authorFactory: "realtimeTrigger" },
337
+ { kind: "resource", authorFactory: "resource" },
338
+ {
339
+ kind: "run.job",
340
+ authorFactory: null,
341
+ absence: "not authorable, on the same grounds as branch/market_item: a run is a NAMED runnable registered against an account (it carries a user and a signature), not an object a workspace bundle carries \u2014 there is no `run` section in a bundle, only an install record. Nothing here targets one either: `s.cloud.job` takes an `image` and a `command` directly, so a containerized job is declared AT THE CALL and needs no separate definition"
342
+ },
343
+ {
344
+ kind: "run.service",
345
+ authorFactory: null,
346
+ absence: "not authorable, for the same reason as run.job \u2014 account-registered state rather than workspace source. A long-running container in a workspace is a `microservice()`"
347
+ },
348
+ { kind: "table", authorFactory: "table" },
349
+ { kind: "table_trigger", authorFactory: "tableTrigger" },
350
+ {
351
+ kind: "tablemap",
352
+ authorFactory: null,
353
+ absence: "unmodeled: a column mapping over a table, used to shape an external schema onto it"
354
+ },
355
+ { kind: "task", authorFactory: "task" },
356
+ { kind: "tool", authorFactory: "tool" },
357
+ { kind: "workflow_test", authorFactory: "workflowTest" },
358
+ { kind: "workspace", authorFactory: "workspaceConfig" },
359
+ { kind: "workspace_trigger", authorFactory: "workspaceTrigger" }
360
+ ];
361
+ var TOTAL_OBJECT_KINDS = ENGINE_OBJECT_KINDS.length;
362
+ var NAMESPACE_NOTES = {
363
+ // The Elasticsearch/OpenSearch note that stood here warned that those
364
+ // credential and target fields were "typed optional but have NO engine
365
+ // default - supply all". They are typed REQUIRED now, so the signature a few
366
+ // lines down says it and a call omitting one does not compile. A warning that
367
+ // restates the type costs every reader of this section and changes nothing an
368
+ // agent writes.
369
+ debug: "`s.debug.stop` is a REAL terminator despite the namespace: it ends the request with an error value. Also exported flat as `die()`; there is no `s.die`.",
370
+ "(top-level)": '`s.throw` raises a named error `s.try_catch` catches; uncaught, it answers HTTP 200 `{ payload, statement: "Throw Error" }` (`s.precondition` sets a status). Inside `s.db.transaction` a throw or failed precondition rolls it back.',
371
+ expect: "Assertions. **Put them in a `workflowTest({...})` stack** \u2014 assert on what a `.call` bound with `as`. They are NOT inert elsewhere: a failure aborts the stack, so an `s.expect.*` in a `query`/`function`/`task` fails the request with HTTP 500 and the assertion's message (`to_equal failed - expected value 2 does not equal 1`) \u2014 remove it. `to_throw` sees only an error carrying a MESSAGE, so an empty-message failure (`ERROR_CODE_ACCESS_DENIED`) reports \"response is ok\", and `to_be_within` EXCLUDES both bounds \u2014 `min < expr < max` \u2014 while `s.security.random_number`'s are inclusive.",
372
+ util: "`s.util.sleep`'s `value` is in SECONDS; a decimal is allowed (`c.decimal(0.5)` = 500 ms).",
373
+ workflow_test: "Run another workflow test from inside one. Pass the `workflowTest()` def handle, not a name."
374
+ };
375
+ var PUBLISHED_AUTHOR_FACTORIES = new Set(
376
+ KIND_DESCRIPTORS.filter((d) => !d.unpublished).flatMap(
377
+ (d) => d.subKinds ? d.subKinds.filter((sub) => !sub.unpublished).map((sub) => sub.authorFactory) : [d.authorFactory]
378
+ )
379
+ );
380
+ var IMPLEMENTED_OBJECT_KINDS = ENGINE_OBJECT_KINDS.filter(
381
+ (k) => k.authorFactory !== null && PUBLISHED_AUTHOR_FACTORIES.has(k.authorFactory)
382
+ );
383
+ function unconfirmedCoverage(statements) {
384
+ const sPaths = statements.filter((s) => UNCONFIRMED_STATEMENTS.has(s.storedName)).map((s) => s.sPath).sort();
385
+ return sPaths.length === 0 ? {} : { unconfirmed: { note: UNCONFIRMED_NOTE, sPaths } };
386
+ }
387
+ function unmodeledObjectKinds() {
388
+ const implemented = new Set(IMPLEMENTED_OBJECT_KINDS.map((k) => k.kind));
389
+ return ENGINE_OBJECT_KINDS.filter((k) => !implemented.has(k.kind)).map((k) => ({
390
+ kind: k.kind,
391
+ absence: k.absence ?? "built but withheld from the published surface"
392
+ }));
393
+ }
394
+ var VALUE_CONSTRUCTORS = [
395
+ { name: "c.text", signature: "(s: string) => Value", description: 'String constant \u2192 tag "const".' },
396
+ { name: "c.int", signature: "(n: number | bigint | string) => Value", description: 'Integer constant \u2192 tag "const:int". The engine stores integers as STRINGS and has no 53-bit limit, so pass a string or bigint for anything past Number.MAX_SAFE_INTEGER \u2014 c.int("9223372036854775807") is exact where the number literal for it is already \u2026808. A number that is not a safe integer THROWS rather than encoding the rounded value. The VALUE encodes at any width, but an `f.int` COLUMN is signed 64-bit and CLAMPS past its ceiling rather than failing \u2014 see `llms/fields.md`.' },
397
+ { name: "c.decimal", signature: "(n: number | string) => Value", description: 'Decimal constant \u2192 tag "const:decimal". Pass a string only to keep a stored spelling a number cannot reproduce (c.decimal("10.00") keeps its trailing zeros).' },
398
+ { name: "c.blank", signature: '(tag: "const:<type>") => Value', description: `The editor's UNCONFIGURED value box (stored value ""), emitted by codegen for a pulled workspace \u2014 do not author it. NOT a zero or an empty collection: the engine reads "" and "0" differently, so c.blank("const:int") \u2260 c.int(0) and neither canonicalizes into the other. Constant tags except const/const:obj, whose blanks are c.text("")/c.obj(null).` },
399
+ { name: "c.bool", signature: "(b: boolean) => Value", description: 'Boolean constant \u2192 tag "const:bool".' },
400
+ { name: "c.null", signature: '(tag?: "const:null" | "const:obj") => Value', description: `Null constant, stored value "null". Bare c.null() is tag "const:null". c.null("const:obj") is the OBJECT-TYPED null the engine writes into a db.* statement's @meta slot \u2014 different stored bytes from c.obj(null), which is the blank object (value ""), though both evaluate to null. Codegen emits whichever spelling the workspace stored; do not swap one for the other.` },
401
+ { name: "c.obj", signature: "(o?: Json | null) => Value", description: 'Object constant \u2192 tag "const:obj". A populated one stores an empty {} carrying one `set` filter per key \u2014 the editor\'s form, and the only populated form the engine reads back (a populated JSON string arrives truncated and fails the request with ERROR_FATAL "Unable to decode."). \u26A0 a ZERO-BASED numeric key is an INDEX in the engine\'s data model, so c.obj({"0":"a"}) evaluates to the list ["a"] (a non-zero-based one like {"2":\u2026} survives as a key) \u2014 that is the platform, not this encoding. Write c.array([...]) for a list, or prefix the keys ("k0") for an object; `export()` warns. No argument = the empty object {} \u2014 use this one. Explicit null = the legacy blank form the engine evaluates to null, NOT {}; it exists only so a pulled workspace round-trips, do not author it. Plain JSON literals only \u2014 a nested tagged value (inp/ref/auth/c.*) is rejected; for a computed object response use a record of values, not c.obj.' },
402
+ { name: "c.array", signature: "(a: Json[]) => Value", description: 'Array constant (JSON string) \u2192 tag "const:array". Plain JSON literals only \u2014 a nested tagged value is rejected, same as c.obj.' },
403
+ { name: "c.expression", signature: "(source: string) => Value", description: `Xano Expression Engine source, passed through VERBATIM \u2192 tag "const:expr2". The string IS the expression: c.expression('"Hi, " ~ $input.name'), c.expression("$var.price * $var.qty"). \u26A0\uFE0F NOT VALIDATED \u2014 never parsed or type-checked, invisible to InferResponse, and untouched by a rename that updates every typed ref(); a typo surfaces at runtime or as a wrong answer. Use it ONLY for syntax the typed surfaces cannot express (~ concatenation, inline arithmetic, conditionals) \u2014 prefer ref/inp/col, withFilters+fl.*, and obj() (which BUILDS a checked expression). Not the expr() condition builder.` },
404
+ { name: "c.expressionLegacy", signature: "(source: string) => Value", legacy: true, description: "the older `const:expr` expression form, emitted by codegen for workspaces that still hold one \u2014 author `c.expression` instead." },
405
+ { name: "c.now", signature: "() => Value", description: "Current time as epoch-ms \u2014 the engine-native const:epochms constant (no filter). Valid inline as a where/cmp operand. For cutoff math (cutoff = now - max_age) either compare inline or, for reuse/readability, hoist it into an s.set_var and compare against the var." },
406
+ { name: "obj", signature: "(fields: Record<string, Value | nested>) => ObjValue<typeof fields>", description: 'Dynamic object value \u2192 tag "const:expr2" (an object-literal expression string). The dynamic sibling of c.obj: members may be inp/ref/auth values, env()/setting()/sys.*, c.now(), c.* constants, nested records, or arrays \u2014 and each member may carry a FILTER CHAIN (withFilters + fl.*), which renders as the expression pipe `$var.row|get:"a.b"`. A null-safe ref(path, { safe: true }) works inside an obj() member directly. The member record rides the return type, so InferResponse (through a set_var too) resolves each member like a top-level response key \u2014 `response: { user: obj({ id: ref("row.id") }) }` derives `{ user: { id: Col | null } }`, and a raw nested object literal (`response: { user: { id: ref("row.id") } }`, auto-wrapped through this) derives the same. A constant record or list has two spellings that both work and render identically: bare (`{ a: 1 }`, `[]`) or `c.obj(...)`/`c.array([...])` \u2014 prefer the bare form. An inner obj() nests, same bytes as a raw record. The legacy blank `c.obj(null)` is refused here (it evaluates to null, not {}) \u2014 write c.null() or c.obj(). Still rejected: a filter ARGUMENT that carries its own chain or is a c.now() (a trailing | binds to the whole value, not one argument, and c.now() needs one), a DISABLED filter (an expression string cannot record that), and the output/response/trycatch/toolset/reg tags (a type error) \u2014 set_var those first and ref() them.' },
407
+ { name: "ref", signature: "(name: string, opts?: { safe?: boolean }) => Value", description: 'Reference a stack variable \u2192 tag "var". Pass { safe: true } for null-safe nested access \u2014 a dotted ref("owner.user_id", { safe: true }) compiles through the get filter so it resolves to null instead of raising "Unable to locate var" when the base is null.' },
408
+ {
409
+ name: "inp",
410
+ signature: "(name: string) => Value",
411
+ description: 'Reference a function/endpoint input \u2192 tag "input". Resolves ONLY against the `input` block of the def it sits in \u2014 a value produced earlier in the stack is `ref("var.field")`, not `inp("field")`. A name that is not declared here deploys clean and fails at runtime with ERROR_FATAL "Unable to locate input: <name>" on every branch that reads it; `export()` warns, and `--strict` fails the build. Sending the name in the request does NOT rescue it \u2014 an undeclared input is never bound, so the call fails identically with the value present. A dotted path drills INTO a declared input (`inp("action.amount")` needs a declared `action`).'
412
+ },
413
+ { name: "col", signature: "(name: string) => Value", description: 'Reference a table column \u2192 tag "col".' },
414
+ { name: "auth", signature: "(path?: string) => Value", description: 'Reference the authenticated identity (auth("id") \u2192 $auth.id) \u2192 tag "auth".' },
415
+ { name: "caught", signature: '(path?: "code" | "message" | "name" | "result") => Value', description: 'Read the caught error inside an s.try_catch CATCH arm \u2192 tag "trycatch". Valid ONLY there \u2014 it reads empty in the try/finally arms and outside the statement. Those four fields are all the engine binds (result is the attached payload); bare caught() is the whole error record. \u26A0 For an ENGINE-raised exception only `code` and `name` are populated; for an `s.throw`, `message` is the fixed string "Throw Error Statement" and your text is in `result`. So `caught("name")` is useful in both cases and `caught("message")` in NEITHER.' },
416
+ { name: "toolset", signature: '(path: "token" | "params" | `params.${string}`) => Value', description: `Read a toolset-scoped binding inside a tool \u2192 tag "toolset". The engine binds two: token (the calling URL's token, null when absent) and params (its parameters, as an object); a dotted params.<key> reads one parameter out of that object. Bound only while a tool runs under its toolset \u2014 anywhere else it reads empty.` },
417
+ { name: "env", signature: "(name: string) => Value", description: 'Read a WORKSPACE environment variable (set via workspaceConfig({ env }) or the dashboard) \u2192 `$env.NAME`. Compiles to tag "setting" with the plain name. env("remote_ip") reads a user var named remote_ip, not the caller IP \u2014 use sys.remoteIp() for that. A name that does not exist resolves to NULL rather than erroring, so a typo deploys clean and fails downstream of the value: prefer typedEnv(config), which turns the names that config declares into properties (E.STRIPE_KEY), and export() warns stack.env-undeclared on any env() or bare setting() outside the declared set once a config declares env at all.' },
418
+ { name: "setting", signature: "(name: string) => Value", description: 'Reference a workspace setting \u2192 tag "setting". Built-in system vars are $-prefixed settings, e.g. setting("$remote_ip"); prefer the typed sys.* accessors.' },
419
+ { name: "sys.*", signature: "() => Value", description: 'Built-in system / request-context variables \u2192 tag "setting" ($-prefixed). Accessors: remoteIp, requestMethod, requestUri, requestQueryString, httpHeaders, requestAuthToken, apiBaseUrl, datasource, branch, tenant, release, platform, isDebugger. In XanoScript these are $env.$remote_ip etc.; sys.remoteIp() is the public-endpoint rate-limit key (auth("id") is null there). sys.httpHeaders() keys arrive Title-Cased whatever the client sent: a client sending `x-sync-secret` is read as `X-Sync-Secret`, so `get:"x-sync-secret"` is null in production \u2014 read the Title-Cased name. s.api.call sends header names the same way, so a workflow test sees what a real request delivers.' },
420
+ { name: "filter", signature: "(name: string, ...args: Value[]) => FilterXdo", description: "Build a filter-chain entry by raw name (escape hatch)." },
421
+ { name: "fl.*", signature: "(...args: Value[]) => FilterXdo", description: "Typed value-pipeline filters; see the `filters` catalog." },
422
+ { name: "withFilters", signature: "(value: Value, ...filters: FilterXdo[]) => Value", description: "Attach a filter chain to a value (filters passed spread; an array is also accepted)." }
423
+ ];
424
+ var FIELD_DESCRIPTORS = [
425
+ { name: "text", stored: "text" },
426
+ { name: "int", stored: "int" },
427
+ { name: "decimal", stored: "decimal" },
428
+ { name: "bool", stored: "bool" },
429
+ { name: "uuid", stored: "uuid" },
430
+ { name: "date", stored: "date" },
431
+ { name: "email", stored: "email" },
432
+ { name: "password", stored: "password" },
433
+ { name: "json", stored: "json" },
434
+ { name: "timestamp", stored: "epochms" },
435
+ { name: "image", stored: "blob_img" },
436
+ { name: "video", stored: "blob_video" },
437
+ { name: "audio", stored: "blob_audio" },
438
+ { name: "attachment", stored: "blob" },
439
+ // Input-only: a raw upload is the request's bytes, not something a table holds.
440
+ { name: "file", stored: "file", inputOnly: true },
441
+ // Input-only: a column linking a whole table is a foreign key (tableRef).
442
+ { name: "dbLink", stored: "<tableGuid>_mvpschema", inputOnly: true },
443
+ { name: "geo.point", stored: "geo_point" },
444
+ { name: "geo.multipoint", stored: "geo_multipoint" },
445
+ { name: "geo.linestring", stored: "geo_linestring" },
446
+ { name: "geo.multilinestring", stored: "geo_multilinestring" },
447
+ { name: "geo.polygon", stored: "geo_polygon" },
448
+ { name: "geo.multipolygon", stored: "geo_multipolygon" },
449
+ { name: "enum", stored: "enum" },
450
+ { name: "vector", stored: "vector" },
451
+ { name: "object", stored: "obj" },
452
+ { name: "tableRef", stored: "int", methodKey: "tableRef" }
453
+ ];
454
+ function buildFieldTypes() {
455
+ return FIELD_DESCRIPTORS.map(({ name, stored, methodKey, inputOnly }) => ({
456
+ name,
457
+ stored,
458
+ methods: Object.keys(FIELD_METHODS[methodKey ?? name] ?? {}),
459
+ ...inputOnly ? { inputOnly: true } : {}
460
+ }));
461
+ }
462
+ function buildFilters() {
463
+ return FILTER_NAMES.map((name) => {
464
+ const spec = FILTER_SPECS[name];
465
+ const entry = { name, fl: `fl.${name}`, typed: !!spec };
466
+ if (spec?.args?.length) entry.args = spec.args;
467
+ if (spec?.variadic) entry.variadic = true;
468
+ if (spec?.result) entry.result = spec.result;
469
+ if (spec?.group) entry.group = spec.group;
470
+ if (spec?.description) entry.description = spec.description;
471
+ return entry;
472
+ });
473
+ }
474
+ var SPECS_BY_NAME = new Map(GENERATED_SPECS.map((s) => [s.name, s]));
475
+ var LEGACY_SURFACES = {
476
+ "api.realtime_event": "publishes to the SUPERSEDED workspace-global realtime layer, NOT to a `realtimeChannel()` \u2014 its `channel` is a string against that layer, so pointing it at a current-layer channel path publishes into the void. Use `s.realtime.publish` instead: it names the owning `realtimeServer()`, so it addresses a real `realtimeChannel()`."
477
+ };
478
+ var OVERRIDDEN_SURFACES = /* @__PURE__ */ new Set([
479
+ "mvp:api_request",
480
+ "mvp:streaming_api_request",
481
+ "mvp:connect_webflow_api_request",
482
+ "mvp:microservice_request"
483
+ ]);
484
+ var STATEMENT_RESULTS = {
485
+ // db.* — mirrors the curated "Runtime behavior" block and InferResponse.
486
+ "db.get": { name: "as", type: "InferRow<T> | null", note: "binds null on a miss, never throws" },
487
+ "db.add": { name: "as", type: "InferRow<T>", note: "the full inserted row incl. id/created_at" },
488
+ "db.edit": { name: "as", type: "InferRow<T>", note: "the full post-mutation row; throws NotFound on a miss" },
489
+ "db.patch": { name: "as", type: "InferRow<T>", note: "the full post-mutation row; throws NotFound on a miss" },
490
+ "db.add_or_edit": { name: "as", type: "InferRow<T>", note: "upserts and never misses" },
491
+ "db.del": { name: "as", type: "null", note: "the engine deletes and returns no value; throws NotFound on a miss" },
492
+ "db.has": { name: "as", type: "boolean" },
493
+ "db.query": { name: "as", type: "InferRow<T>[]", note: "a paging envelope when metadata paging is on" },
494
+ "db.bulk.patch": { name: "as", type: "InferRow<T>[]" },
495
+ "db.bulk.delete": { name: "as", type: "number", note: "count of deleted rows" },
496
+ "db.increment": { name: "as", type: "InferRow<T>[] | number", note: 'the updated rows, or the changed-row count with returnType "count"' },
497
+ // security.check_password binds a boolean (does the plaintext match the
498
+ // stored hash).
499
+ "security.check_password": {
500
+ name: "as",
501
+ type: "boolean",
502
+ note: "true when the plaintext matches the stored hash. \u26A0 input.password double-hashes \u2014 pass input.text() plaintext"
503
+ },
504
+ // redis/storage binds measured live: a get miss is `false`, not null.
505
+ "redis.get": { name: "as", type: "unknown", note: "FALSE (not null) on a miss" },
506
+ "redis.set": { name: "as", type: "boolean", note: "false when create_only meets an existing key (kept)" },
507
+ "storage.read_file_resource": { name: "as", type: "{ name; size; mime; data: <contents> }" },
508
+ // util.* — three whose UNITS, SHAPE or PHASE cost teams time, and none is
509
+ // guessable from the signature.
510
+ "util.geo_distance": {
511
+ name: "as",
512
+ type: "number",
513
+ note: "great-circle distance in METRES (a decimal) \u2014 divide by 1000 for km. Identical points return 0"
514
+ },
515
+ "util.get_all_input": {
516
+ name: "as",
517
+ type: "unknown",
518
+ note: "inside an ATTACHED middleware it is a { type, vars } envelope whose vars DIFFERS BY PHASE: pre \u2192 the request inputs; post \u2192 { status, result } (plus payload on an error), NOT the request \u2014 read the request in post with s.util.get_raw_input. See llms/kinds-core.md"
519
+ },
520
+ "util.ip_lookup": {
521
+ name: "as",
522
+ type: "IpLookupResult | null",
523
+ note: 'NESTED, not flat: { continent: {code,name}, country: {code,name}, region: {code,name}, city: {name}, postal: {code}, location: {latitude, longitude, tz, radius} } \u2014 ref("geo.location.latitude"), ref("geo.city.name"); radius is KILOMETRES. \u26A0 Every leaf is nullable (region/city/postal often null on a normal hit). `city` is an OBJECT: a bare ref("geo.city") into a text column fails and { safe: true } does NOT help; drill to city.name. The whole var is null for an unresolvable address'
524
+ },
525
+ // Clearly-typed declarative ops.
526
+ "array.every": { name: "as", type: "boolean" },
527
+ "math.add": { name: "as", type: "number" },
528
+ "math.bitwise.and": { name: "as", type: "number" },
529
+ "math.bitwise.or": { name: "as", type: "number" },
530
+ "math.bitwise.xor": { name: "as", type: "number" },
531
+ "object.keys": { name: "as", type: "string[]" },
532
+ "object.values": { name: "as", type: "unknown[]" },
533
+ "object.entries": { name: "as", type: "[string, unknown][]" }
534
+ };
535
+ var CONTEXT_FIELD_ENUMS = {
536
+ "mvp:precondition:error_type": PRECONDITION_ERROR_TYPES,
537
+ // `access` defaults to `public`, so a misspelling fails OPEN — the file is
538
+ // served to the world. The members were unlisted anywhere, leaving `private`
539
+ // inferable only from `s.storage.sign_private_url`'s existence.
540
+ "mvp:create_attachment:access": STORAGE_ACCESS,
541
+ "mvp:create_audio:access": STORAGE_ACCESS,
542
+ "mvp:create_image:access": STORAGE_ACCESS,
543
+ "mvp:create_video:access": STORAGE_ACCESS,
544
+ "mvp:setheader:duplicates": HEADER_DUPLICATES
545
+ };
546
+ function fieldsOf(spec) {
547
+ return spec.rules.map((r) => {
548
+ const f = {
549
+ name: r.field,
550
+ type: r.type,
551
+ optional: r.optional || r.default !== void 0
552
+ };
553
+ if (r.default !== void 0) f.default = r.default;
554
+ const curated = CONTEXT_FIELD_ENUMS[`${spec.name}:${r.field}`];
555
+ if (r.enum !== void 0) f.enum = r.enum;
556
+ else if (curated) f.enum = [...curated];
557
+ return f;
558
+ });
559
+ }
560
+ function buildManifest(opts = {}) {
561
+ const objectKinds = KIND_DESCRIPTORS.filter((d) => !d.unpublished).map((d) => ({
562
+ ...d,
563
+ registered: isRegisteredKind(d.kind),
564
+ // A published kind can still have unpublished sub-kinds (the two realtime
565
+ // lifecycle trigger types under `trigger`), so filter that level too.
566
+ ...d.subKinds ? { subKinds: d.subKinds.filter((sub) => !sub.unpublished) } : {}
567
+ }));
568
+ const statements = STATEMENT_SURFACES.map(([surface, storedName]) => {
569
+ const spec = SPECS_BY_NAME.get(storedName);
570
+ const overridden = OVERRIDDEN_SURFACES.has(storedName);
571
+ const entry = {
572
+ surface,
573
+ storedName,
574
+ sPath: sPathOf(surface),
575
+ registered: isRegisteredStatement(storedName),
576
+ declarative: spec !== void 0 && !overridden
577
+ };
578
+ if (spec) {
579
+ entry.output = spec.output ?? false;
580
+ if (!overridden) entry.fields = fieldsOf(spec);
581
+ }
582
+ if (Object.hasOwn(STATEMENT_RESULTS, surface)) entry.result = STATEMENT_RESULTS[surface];
583
+ if (Object.hasOwn(LEGACY_SURFACES, surface)) entry.legacy = true;
584
+ return entry;
585
+ });
586
+ const filters = buildFilters();
587
+ return {
588
+ name: "xanosdk",
589
+ version: opts.version ?? "0.0.0",
590
+ description: "TypeScript SDK that compiles a typed Xano workspace into the importable packageExport JSON bundle.",
591
+ coverage: {
592
+ // Counted over the ENGINE's catalog, not over this SDK's kinds: the engine
593
+ // has one object kind per trigger type where the SDK has one `trigger`
594
+ // kind with sub-kinds, so counting SDK kinds reported 16/30 for a surface
595
+ // that actually authors 23 of them.
596
+ objectKinds: {
597
+ implemented: IMPLEMENTED_OBJECT_KINDS.length,
598
+ total: TOTAL_OBJECT_KINDS,
599
+ unmodeled: unmodeledObjectKinds()
600
+ },
601
+ statements: {
602
+ implemented: statements.filter((s) => s.registered).length,
603
+ total: TOTAL_STATEMENTS,
604
+ // Mapped through the built entries rather than derived from the stored
605
+ // names directly, so a name that no longer has an authoring surface
606
+ // cannot be published as an `s.` path that does not resolve.
607
+ ...unconfirmedCoverage(statements)
608
+ },
609
+ filters: { typed: filters.filter((f) => f.typed).length, total: filters.length }
610
+ },
611
+ values: { constructors: [...VALUE_CONSTRUCTORS], tags: TAGS },
612
+ objectKinds,
613
+ fieldTypes: buildFieldTypes(),
614
+ statements,
615
+ filters,
616
+ cli: buildCli(),
617
+ cliGlobalFlags: buildCliGlobalFlags()
618
+ };
619
+ }
620
+ var DEFAULT_KEEP = /* @__PURE__ */ new Set([
621
+ "storage.create_image:access",
622
+ "storage.create_attachment:access",
623
+ "storage.create_audio:access",
624
+ "storage.create_video:access"
625
+ ]);
626
+ function preconditionStatusLine() {
627
+ const byStatus = /* @__PURE__ */ new Map();
628
+ for (const type of PRECONDITION_ERROR_TYPES) {
629
+ const status = PRECONDITION_ERROR_STATUS[type];
630
+ byStatus.set(status, [...byStatus.get(status) ?? [], type]);
631
+ }
632
+ return [...byStatus].sort(([a], [b]) => a - b).map(([status, types]) => `${types.map((t) => `\`${t}\``).join("/")} \u2192 ${status}`).join(", ");
633
+ }
634
+ var fieldLine = (f, sPath) => {
635
+ const keepDefault = f.default !== void 0 && DEFAULT_KEEP.has(`${sPath}:${f.name}`);
636
+ const type = f.enum ? f.enum.map((v) => JSON.stringify(v)).join(" | ") : f.type;
637
+ return `${f.name}${f.optional ? "?" : ""}: ${type}${keepDefault ? ` = ${JSON.stringify(f.default)}` : ""}`;
638
+ };
639
+ var FILTER_NOTES = {
640
+ // The registry-only array family. `array_find` is the dangerous one; `splice`
641
+ // returns the opposite of what its name suggests in one of its two forms; and
642
+ // `array_upmerge` is the only one of the family that writes INTO an element
643
+ // rather than replacing it.
644
+ array_find: "a MISS THROWS (`Unable to find element.`) rather than yielding null \u2014 use `array_has` to test first",
645
+ array_filter: "matches by EXAMPLE (an object of key/value pairs), not by code, and compares LOOSELY",
646
+ splice: "returns the REMOVED slice, not the remainder \u2014 [1,2,3,4] spliced at offset 1 length 2 yields [2,3]; with a `path` it removes in place and returns the whole value instead",
647
+ array_upmerge: "merges the node INTO each matched element; `array_replace` swaps the element whole",
648
+ array_set: "sets a DOTTED path and returns the value; a non-object piped value is replaced by an empty one",
649
+ // A computed key is the grouping half of `index_by`: one pass over the rows,
650
+ // then a lookup per group, with no query per group.
651
+ get: '`path` may be a runtime value: after `fl.index_by("suite_id")`, `fl.get(ref("suite.id"))` returns that key\'s rows (null for a missing key)',
652
+ // "Direction" family — which operand is the subject is genuinely confusing.
653
+ contains: "piped value is the subject text; the arg is the substring searched for",
654
+ ends_with: "piped value is the subject text; the arg is the substring searched for",
655
+ starts_with: "piped value is the subject text; the arg is the substring searched for",
656
+ icontains: "case-insensitive; piped value is the subject, the arg is the substring",
657
+ // The regex family REVERSES that direction, which is the whole reason it needs
658
+ // its own note: the pattern is piped and the subject is the argument. Written
659
+ // the other way round it reads correctly, type-checks, and answers false for
660
+ // every input, so the note has to state the order and the symptom.
661
+ regex_test: 'piped value is the PATTERN (build it with `c.regex(...)`); the arg is the subject \u2014 the REVERSE of `contains`/`starts_with`. Swapped, it answers false for every input with no error, so write `withFilters(c.regex("^a+$"), fl.regex_test(inp("s")))` (or name the arg: `fl.regex_test({ subject: inp("s") })`). A pattern found in the subject slot is refused at build time',
662
+ regex_match: "piped value is the PATTERN, the arg is the subject \u2014 see `regex_test`",
663
+ regex_match_all: "piped value is the PATTERN, the arg is the subject \u2014 see `regex_test`",
664
+ regex_replace: "piped value is the PATTERN, `subject` is the text searched \u2014 see `regex_test`. The replacement comes FIRST",
665
+ iends_with: "case-insensitive; piped value is the subject, the arg is the substring",
666
+ istarts_with: "case-insensitive; piped value is the subject, the arg is the substring",
667
+ // "empty" is a specific set of values, not just null.
668
+ filter_empty: 'keeps entries that are not empty ("", null, 0, "0", false, [], {})',
669
+ first_notempty: 'first value that is not empty ("", null, 0, "0", false, [], {})',
670
+ // The `code` arg is a JS FUNCTION BODY (it must `return`), not a column path,
671
+ // and which identifiers it can see depends on the filter — see **Lambda bodies**.
672
+ // Build it with `lam.fn`, which makes the bindings the function's parameters.
673
+ map: "`code` is a JS body run per element, over `$this`/`$index`/`$parent` \u2014 build it with `lam.fn`",
674
+ filter: "`code` is a JS body run per element (keep it? true/false), over `$this`/`$index`/`$parent`",
675
+ every: "`code` is a JS body run per element (true for all?), over `$this`/`$index`/`$parent`",
676
+ some: "`code` is a JS body run per element (true for any?), over `$this`/`$index`/`$parent`",
677
+ find: "`code` is a JS body run per element; returns the first element it accepts",
678
+ findIndex: "`code` is a JS body run per element; returns the first matching index",
679
+ reduce: "`code` is a JS body run per element; the ACCUMULATOR is `$result` (there is no `$acc`) and `initial_value` is REQUIRED \u2014 omitting it would slot the code as the initial value",
680
+ lambda: "runs a JS body once over the piped value, which it binds as `$this` (NOT `$parent`)",
681
+ // The one filter next to `lambda` that is NOT a lambda. It reads as one, its
682
+ // upstream description names a `$this` that does not exist on its path, and
683
+ // both wrong spellings can return a plausible value with HTTP 200 — so the
684
+ // note has to say what the binding IS, not only what it isn't.
685
+ transform: '`expression` is Xano Expression Engine source, NOT a JS body \u2014 no `return`, and the piped value is `$0` (or `$$`), NOT `$this` (which is null here). `$var`/`$input`/`$env`/`$auth` resolve and filters pipe inside it: `$0 * 2`, `$0|sort|join:","`. Parenthesize a pipe inside an object literal \u2014 `{ s: ($0|sort|join:",") }` \u2014 or its comma is read as the key separator and later keys vanish silently. For JavaScript use `lambda`',
686
+ // The sort mode is the whole behavior of this filter, and picking it wrong is
687
+ // SILENT — every unrecognized spelling falls through to `itext`, so the array
688
+ // comes back sorted as case-insensitive text with no error anywhere. That is
689
+ // how "top N by score/distance/recency" comes out wrong.
690
+ fsort: '`type` is the comparator, and ONLY "number" compares numerically \u2014 "text"/"itext" are strcmp/strcasecmp, "natural"/"inatural" are the human-readable "a2 < a10" orderings. Default "itext". Anything else silently sorts as text, so a numeric sort MUST spell "number"; the path arg drills into each element',
691
+ // The CSV pair reads as interchangeable and is not: only `csv_create` writes a
692
+ // header, and `csv_encode`'s per-row column order misaligns heterogeneous rows
693
+ // with no error at all.
694
+ csv_encode: "writes NO header \u2014 values only, each row in THAT row's key order with no normalization across rows, so rows whose keys differ in order or count silently misalign columns. Nested cells are JSON-encoded and `false` writes empty. A piped array of SCALARS is treated as one row. Use `csv_create` for a header",
695
+ csv_create: "the header-writing counterpart to `csv_encode`: the PIPED value is the list of column names (written as the header line) and `rows` carries the data rows",
696
+ // A group-by whose name reads as a lookup table. The singular spelling
697
+ // `idx[key].name` is null at runtime rather than an error.
698
+ index_by: "a GROUP-BY: every value is an ARRAY of the items sharing that key, even when only one does, so a lookup reads `idx[key][0]`. Items whose path is missing or non-scalar are dropped",
699
+ // Non-obvious names.
700
+ epochms_transform: 'applies a relative shift (e.g. "+1 day") to the timestamp',
701
+ unpick: "returns the object without the named keys (inverse of a pick)"
702
+ };
703
+ function wrapList(items, indent, width = 84) {
704
+ const lines = [];
705
+ let line = "";
706
+ for (const item of items) {
707
+ const next = line === "" ? indent + item : `${line} \xB7 ${item}`;
708
+ if (next.length > width && line !== "") {
709
+ lines.push(line + " \xB7");
710
+ line = indent + item;
711
+ } else {
712
+ line = next;
713
+ }
714
+ }
715
+ if (line !== "") lines.push(line);
716
+ return lines;
717
+ }
718
+ function renderLambdaSection() {
719
+ const lines = ["## Lambda bodies (JavaScript)", ""];
720
+ lines.push(
721
+ "**A lambda is an escape hatch, not a default.** The body runs outside the request's own",
722
+ "runtime, and a workspace has a BOUNDED pool of lambda workers every lambda in it shares",
723
+ "\u2014 so a call both crosses a process boundary and draws on a workspace-wide resource.",
724
+ "Reach for one only when the typed surface cannot express the work: if a native filter,",
725
+ "an `expr(...)`/`obj(...)` expression, or a plain statement can, use that. The crossing",
726
+ "is per CALL, not per element \u2014 an iterating filter sends the body ONCE and loops on the",
727
+ "other side, so one body over a whole list beats one called from inside a stack loop.",
728
+ "",
729
+ "The lambda statement (`s.lambda({ as, code, timeout? })`) and eight filters run a",
730
+ "JavaScript body. **Write the body as a FUNCTION, not a `c.text` string** \u2014 the",
731
+ "bindings are its parameters, so the editor supplies them and a wrong name is a",
732
+ "compile error instead of a wrong value at runtime. Write it inline and the surface",
733
+ "is implied by where it sits; nothing names one:",
734
+ "",
735
+ "```ts",
736
+ "fl.map(({ $this }) => $this * 2) // map's bindings, typed from the position",
737
+ "fl.reduce({ initial_value: 0, code: ({ $result, $this }) => $result + $this })",
738
+ 's.lambda({ as: "total", code: ({ $var }) => $var.subtotal * 1.2 }) // ambient only \u2014 $this is a compile error',
739
+ "```",
740
+ "",
741
+ "The parameters are a fiction \u2014 only the BODY is sent, and the engine injects the",
742
+ "bindings as free identifiers \u2014 so DESTRUCTURE them as named: `(b) => b.$this`,",
743
+ "`{ $this: x }`, nesting, a default or rest are undefined there (refused).",
744
+ "",
745
+ "\u26A0 An inline `code:` arrow receives BINDINGS ONLY. `capture` is an option of",
746
+ "`lam.fn`, not a field of `s.lambda` or a filter, so `capture:` beside `code:` is a type",
747
+ "error \u2014 the fix is to relocate the body, not to drop the field: `lam.fn(fn, { capture })` (below).",
748
+ "",
749
+ "For a body built away from its call site:",
750
+ "",
751
+ "- `lam.fn(({ $result, $this }) => $result + $this, { surface?, capture? })` \u2014 name a `surface` to check it here, or omit it and the call site checks it.",
752
+ '- `lam.raw("return 1", { surface })` \u2014 text, same validation.',
753
+ '- `lam.file("./lambdas/total.ts")` \u2014 a default-exported function in its own module (path relative to the caller), read as text at build time. NODE ONLY: `import { lam } from "@xano/sdk/node"` (isomorphic `lam` has no `file`). Only the default export\'s BODY is sent, so a value import, a second export or a top-level helper is refused \u2014 move helpers inside; `import type` / `import { type X }` are free. `@xano/sdk/lambda-globals` types the globals below program-wide: keep modules in `xano/lambdas/` (its scaffold tsconfig loads it) or `import type {} from` it under their own tsconfig.',
754
+ "",
755
+ "Nothing from the enclosing scope crosses: the body is sent as TEXT, so a closed-over",
756
+ "`const rate` is undefined there (a wrong VALUE at HTTP 200, not an error). Put what the",
757
+ "body needs in `capture`; it arrives as the SECOND parameter, emitted as a `const` prelude:",
758
+ "",
759
+ "```ts",
760
+ 'lam.fn(({ $this }, { capturedRate }) => $this * capturedRate, { surface: "map", capture: { capturedRate: rate } })',
761
+ "```",
762
+ "",
763
+ "\u26A0 A capture key must NOT share its name with a module-scope binding: a `.ts` loader",
764
+ "renames one of two same-named bindings, so the body reads `rate2` while the prelude",
765
+ "declares `rate` (refused at build time). `capture: { capturedRate: rate }` is the safe form.",
766
+ "",
767
+ "Capture JSON data only: a function, `NaN`, a sparse/typed array or class instance (`Date`, `Map`\u2026)",
768
+ "is refused at build time, at any depth. Capture the plain form and rebuild in",
769
+ "the body (`d.getTime()` \u2192 `new Date(d)`). The captured type flows into the second parameter.",
770
+ "",
771
+ "A body is a FUNCTION BODY: it must `return` its value. Bindings by surface \u2014 an",
772
+ "identifier outside its surface's set is undefined at runtime, and the SDK refuses",
773
+ "it at build time whichever spelling you use:",
774
+ ""
775
+ );
776
+ const ambient = LAMBDA_BINDINGS["s.lambda"];
777
+ const tick = (x) => `\`${x}\``;
778
+ lines.push(
779
+ `- every surface: ${ambient.map(tick).join(" \xB7 ")} (+ the ${LAMBDA_GLOBALS.map(tick).join(" / ")} globals)`
780
+ );
781
+ const extras = (surface) => LAMBDA_BINDINGS[surface].filter((b) => !ambient.includes(b));
782
+ const byExtras = /* @__PURE__ */ new Map();
783
+ for (const surface of Object.keys(LAMBDA_BINDINGS)) {
784
+ if (surface === "s.lambda") continue;
785
+ const key = extras(surface).join(" ");
786
+ byExtras.set(key, [...byExtras.get(key) ?? [], surface]);
787
+ }
788
+ for (const [key, surfaces] of byExtras) {
789
+ const label = surfaces.map((x) => tick(x.includes(".") ? x : `fl.${x}`)).join(" \xB7 ");
790
+ lines.push(`- ${label}: + ${key.split(" ").map(tick).join(" \xB7 ")}`);
791
+ }
792
+ lines.push(
793
+ `- \`s.lambda\`: ambient only \u2014 no \`$this\`, no \`$parent\`, no \`$result\`.`,
794
+ "",
795
+ "`$result` is `reduce`'s ACCUMULATOR (there is no `$acc`). `$this` is the element in",
796
+ "an iterating filter and the piped value in `fl.lambda`; `$parent` is the whole array",
797
+ "and exists only on the iterating filters. A stack variable is reached as",
798
+ "`$var.name` \u2014 it is NOT also injected as a bare `$name`.",
799
+ "",
800
+ "Four hazards and the dependency route, all live-verified:",
801
+ "",
802
+ "- \u26A0 A body that THROWS does not fail the request: the engine returns its diagnostic",
803
+ " TEXT as the value with HTTP 200, so the failure reads as bad data. Validate before",
804
+ " consuming a lambda result numerically, and prefer a `lam.*` body, which cannot fail",
805
+ " this way for a binding reason.",
806
+ "- \u26A0 `timeout` is COOPERATIVE \u2014 observed only at an `await` \u2014 so it bounds WAITING (a slow",
807
+ " `fetch`), not compute: a 1s `timeout` over a 3s busy-loop runs all 3s and returns",
808
+ " normally. Bound a loop that could run away inside the body.",
809
+ "- \u26A0 A top-level `import`/`export` is a syntax error \u2014 the body is a function body. Reach",
810
+ ' a dependency through the PRELOADED globals below. A dynamic `import("\u2026")`/`require("\u2026")`',
811
+ " with a LITERAL specifier is not portable: an instance that bundles the body first",
812
+ ' returns the TEXT `Could not resolve "node:crypto"` with HTTP 200.',
813
+ "- Preloaded globals, live-probed \u2014 no specifier, so these work everywhere:",
814
+ ...wrapList(LAMBDA_MODULE_GLOBALS.map(tick), " "),
815
+ " \u2026plus `fetch`, `Buffer`, `TextEncoder`/`TextDecoder`, and the `crypto` above",
816
+ " (`randomUUID`, `createHmac`, `createHash`, `subtle`). `Object.keys(globalThis)`",
817
+ " inside a body lists whatever else a given instance carries.",
818
+ "- \u26A0 `console` output goes to the request LOG, not stdout. `log` \xB7 `error` \xB7 `warn` \xB7",
819
+ " `info` \xB7 `debug` \xB7 `trace` all route there; any other `console` method is undefined,",
820
+ " and CALLING it throws (error text as the value, HTTP 200).",
821
+ "",
822
+ "TypeScript annotations survive in the body, and top-level `await` works.",
823
+ ""
824
+ );
825
+ return lines;
826
+ }
827
+ var NAVIGATION_SLOT = "<!--navigation-->";
828
+ function renderTestsSection() {
829
+ return [
830
+ "## Saved unit tests",
831
+ "",
832
+ "Named input sets run against one object, with assertions on its response \u2014 the tests the Xano editor shows. NOT `workflowTest()`, which is a standalone object with its own stack that calls other objects.",
833
+ "",
834
+ "- `tests?: TestDef[]` on `query()`, `defineFunction()`, and `middleware()`. No other kind stores them.",
835
+ "- `{ name, id?, description?, datasource?, input?, expect?, token? }`.",
836
+ "- `name` is unique within the object and is the key a statement's `mock` resolves against; a duplicate throws.",
837
+ "- `id` defaults to a derivation from the owning object plus the name. A pulled test carries the id Xano minted, emitted explicitly.",
838
+ '- `datasource` defaults to `""` \u2014 an EMPTY datasource. Any other value names one the engine CLONES before the run; `"live"` warns `test.live-datasource` on the owning def.',
839
+ '- \u26A0 That empty default means **no `table({ seed })` rows exist while a unit test runs**, exactly as for a `workflowTest()` \u2014 every `db` read misses, so `resp("0.id")` fails though the deployed endpoint returns those rows. **To read `table({ seed })` rows on an EPHEMERAL**, set `datasource: "live"`: there it holds only the seed fixtures (plus rows `--keep-data` kept); the run\'s writes are dropped. `diagnostics.allow` accepts its warning. \u26A0 `datasource` is STORED, not a per-run flag \u2014 a test left on `"live"` clones the REAL database once it runs against an instance, so clear it before you promote. Otherwise create the rows INSIDE the run (a `defineFunction` fixture the stack calls first) or `mock` the read.',
840
+ "- `input` is `{ <input name>: Value }` \u2014 tagged values (`c.*`, `ref`, \u2026), never plain JS scalars.",
841
+ "- `token` runs the test as an authenticated caller. A pull does NOT bring it back: it is an expiring credential, reported as a deliberate omission.",
842
+ "",
843
+ "### Assertions: `expect.*`, not `s.expect.*`",
844
+ "",
845
+ "- `expect.*` builds a `{type, vars}` record stored on a `tests` entry. `s.expect.*` builds a STATEMENT for a `workflowTest()` stack. Different return types; not interchangeable.",
846
+ "- Subject first \u2014 argument order is the assertion: `expect.to_equal(resp(), c.int(2))`.",
847
+ '- `resp()` is the response under test; `resp("id")` drills, `resp("a-b")` takes the bracket escape.',
848
+ "- Subject only: `to_be_defined`, `to_not_be_defined`, `to_be_empty`, `to_be_null`, `to_not_be_null`, `to_be_true`, `to_be_false`, `to_be_in_the_future`, `to_be_in_the_past`.",
849
+ "- Subject + operand: `to_equal`, `to_not_equal`, `to_be_greater_than`, `to_be_less_than`, `to_contain`, `to_start_with`, `to_end_with`, `to_match` (operand is a delimiter-wrapped pattern \u2014 build it with `c.regex`).",
850
+ "- `expect.to_be_within(subject, min, max)` \u2014 both bounds EXCLUSIVE; a subject equal to either fails.",
851
+ '- `expect.to_throw(exception?)` takes NO subject. With an argument the error message must CONTAIN that text (case-insensitive substring); with none, any error passes. `exception` is a `Value` \u2014 `expect.to_throw(c.text("Wrong password"))`, not a bare string (a bare one fails to compile with TS2345).',
852
+ "",
853
+ "### Mocks",
854
+ "",
855
+ "A statement returns a value instead of doing its work, for one named test.",
856
+ "",
857
+ '- `s.set_var("x", c.int(1), { mock: { "adds one": c.int(123) } })`. Every statement takes `mock`.',
858
+ "- Keyed by TEST NAME. A name the object does not declare THROWS at encode: the engine ignores a mock whose key is not a real test id, so it would deploy clean and silently never apply.",
859
+ "- `{ value, enabled: false }` keeps a mock stored but switched off.",
860
+ "- A mock applies ONLY while its test runs. It changes nothing about a normal request.",
861
+ "",
862
+ "### `example`",
863
+ "",
864
+ "`example?: { input?, output? }` on `query()` \u2014 the saved request/response sample the editor records. Free-form JSON, not tagged values. A pull DOES bring it back.",
865
+ "",
866
+ "### What a `workflowTest()` run actually sees",
867
+ "",
868
+ "The run is isolated in ways that make a correct test fail for reasons the failure message does not name.",
869
+ "",
870
+ '- The run uses an EMPTY datasource by default (`datasource: ""`), so **no `table({ seed })` rows exist while it runs** (same as a unit test, above): a read of seeded row 1 fails with its own precondition message, not an empty-database error. Build fixtures INSIDE the test, or set `datasource: "live"` under the ephemeral-only rule above.',
871
+ '- `s.api.call` does NOT raise when the endpoint answers with an error. It BINDS the error envelope (`{code, message}`) to its `as` and carries on, so a later `s.expect.to_be_defined({ expr: ref("r.field") })` reports the ASSERTION while the real failure was the call, four statements up. Assert on the envelope \u2014 `s.expect.to_contain({ expr: ref("r.code"), value: c.text("ERROR_CODE_INPUT_ERROR") })` \u2014 when a call may fail. `s.function.run` raises instead; the two disagree.',
872
+ '- `s.expect.to_throw({ body, exception? })` runs `body` in an ISOLATED var stack, so a variable bound EARLIER in the test is not visible inside it \u2014 bind what the body needs inside the body. An outer one raises `Missing var entry: <name>` there, which the test reports as `to_throw` not matching (`export()` warns). `exception` is a `Value` whose text the raised message must CONTAIN (`c.text("already exists")`, not a bare string); omit it to accept any error.',
873
+ "- `s.expect.to_throw` catches such a call only when the error carries a MESSAGE. `ERROR_CODE_ACCESS_DENIED` arrives with an empty one, so `to_throw` around an auth-refused call reports `to_throw failed - response is ok` \u2014 which reads as a broken auth gate on a gate that works.",
874
+ "- An endpoint's `auth` gate is NOT enforced on `s.api.call`. A `query({ auth: users })` runs anyway and fails only where its stack dereferences `auth(...)`. A stack that never touches `auth(...)` runs unauthenticated and passes.",
875
+ "- Neither `auth.token` nor an `Authorization` entry in `headers` authenticates the call \u2014 a token that answers 200 over real HTTP is refused here. To cover auth-gated logic, move the body into a `defineFunction` taking the user id and `s.function.call` that; the gate itself is not reachable from a workflow test.",
876
+ "",
877
+ "### Running them",
878
+ "",
879
+ "`xanosdk test run-all` runs the unit tests AND the `workflowTest()` objects an environment carries. It takes no entry file and compiles nothing: it runs what is DEPLOYED, so deploy before testing.",
880
+ "",
881
+ '- `--on <backend>` picks the backend \u2014 `--on ephemeral:<name>`, `--on local-engine`, `--on tenant:<name>`, `--on workspace`; omitted, the one this project last deployed to (grammar: **Backends** in `llms.txt`). \u26A0 `workspace` runs against the REAL database: safe only while every test keeps `datasource: ""`, since a stored non-empty datasource is CLONED before the run.',
882
+ '- `xanosdk test list` shows what is there without running it; `test run "<name>"` runs one; an ambiguous name prints qualified forms (`function:math/happy path`, `workflow:<name>`) `run` accepts.',
883
+ "- `--kind unit|workflow` narrows to one family. `--concurrency <n>` defaults to 1: tests share the environment database.",
884
+ "- A failing suite exits 5; exit 6 when the suite could not be reached \u2014 retry it; a backend that can't be looked up exits 8 (see errors). Having no tests is success.",
885
+ "- For CI: the exit code says THAT something failed, the JSON says WHICH. Progress goes to stderr; stdout carries one JSON document when not a terminal, or with `--json`. `run-all` and `run`: `{ kind, env, name, display, total, passed, failed, tests: [{ kind, name, qualified, object?, status: \"pass\"|\"fail\", message?, expectations?: [{ index, status, message? }], timing? }] }` (`message`: the first failure; `expectations`: a unit test's every expectation in order, `index` from 0; `env`: an ephemeral's or tenant's name; `name`: the backend's own, a local engine's too; null if none), same keys on an empty suite. `list` is `{ kind, env, name, display, total, tests }`; `deploy --test` nests the run under `testRun`.",
886
+ "- `xanosdk deploy ./index.ts --test` deploys and then runs the suite against what it just shipped. A failure exits 5 WITHOUT retracting the deploy \u2014 the environment is live either way.",
887
+ ""
888
+ ];
889
+ }
890
+ function renderSections(m) {
891
+ const lines = [];
892
+ lines.push(`# ${m.name} v${m.version}`, "");
893
+ lines.push(`> ${m.description}`, "");
894
+ lines.push(
895
+ "Xano is a hosted backend platform: one WORKSPACE serves a managed PostgreSQL",
896
+ "database, HTTP API endpoints, background tasks, realtime websocket channels, AI",
897
+ "agents and MCP servers, file storage, and redis. This SDK authors that workspace in",
898
+ "TypeScript \u2014 typed def objects registered on a `workspace(name)` \u2014 compiled to one",
899
+ "importable bundle by `xanosdk export ./xano/index.ts`, shipped live by `xanosdk deploy`.",
900
+ "",
901
+ "Def modules execute at BUILD time only: `s.*` factories return data, and the engine",
902
+ "runs the compiled stack per request \u2014 statements in order, a statement's `as:`",
903
+ "naming a runtime variable, `response` the HTTP body. Every dynamic operand is a",
904
+ 'TAGGED value \u2014 `ref("x")` a stack variable, `inp("x")` an input, `auth("id")` the',
905
+ "caller, `c.*` a constant \u2014 resolved at request time; JS operators over them do not",
906
+ "compute (see Gotchas). Requests share no memory \u2014 state persists in tables or redis.",
907
+ "",
908
+ // The shortfall by name: an agent that cannot see WHICH kinds are missing
909
+ // will invent a factory for one. Reasons live in `manifest.json`.
910
+ `Authorable: statement surfaces ${m.coverage.statements.implemented}/${m.coverage.statements.total}, filters ${m.coverage.filters.total} (${m.coverage.filters.typed} typed).`,
911
+ `Not authorable here: ${m.coverage.objectKinds.unmodeled.map((k) => k.kind).join(", ")} \u2014 these cannot be authored and do not survive a pull; reasons in \`coverage.objectKinds.unmodeled\`.`,
912
+ "",
913
+ "This file is the whole always-read surface: the mental model, the deploy contract,",
914
+ "every cross-cutting gotcha, and control flow. Per-surface detail lives in the topic files listed",
915
+ "below \u2014 open the one whose condition matches the task, skip the rest. For",
916
+ "exhaustive per-entry detail in NEITHER \u2014 a statement's field schema with engine",
917
+ "defaults, a filter's full argument list, the `storedName` mapping \u2014 do a TARGETED",
918
+ "lookup in the shipped `manifest.json` (a program imports it as",
919
+ '`@xano/sdk/manifest.json`, needing `with { type: "json" }` in Node ESM;',
920
+ "it is ~70k tokens, so never read it whole). Its top-level keys are `" + Object.keys(m).join("`, `") + "`. `statements` and `filters` are ARRAYS, not maps \u2014 SELECT, do not index:",
921
+ ` jq '.statements[] | select(.sPath=="db.get")' manifest.json`,
922
+ ` jq '.filters[] | select(.name=="json_decode")' manifest.json`,
923
+ `\u26A0 \`fields\` is null on the ${m.statements.filter((st) => st.declarative !== true).length} \`declarative: false\` statements (\`db.get\`, \u2026): typed wrappers whose arguments are the factory's \`.d.ts\` signature. Null \u2260 missing.`,
924
+ `Select a statement on \`sPath\` (the \`s.*\` path you write), NOT \`surface\` (the XanoScript term): ${m.statements.filter((s) => s.surface !== s.sPath).length} of ${m.statements.length} differ \u2014 \`var\`\u2192\`set_var\`, \`break\`\u2192\`foreach_break\`, \`foreach.remove\`\u2192\`foreach_remove\`, and every \`expect.*\`.`,
925
+ "",
926
+ NAVIGATION_SLOT
927
+ );
928
+ lines.push(
929
+ "## Quickstart",
930
+ "",
931
+ "Authoring is **declarative def-objects** passed to factories \u2014 there is no",
932
+ "callback/chaining builder.",
933
+ "",
934
+ "```ts",
935
+ 'import { workspace, apiGroup, query, table, input, f, ref, inp, auth, s } from "@xano/sdk";',
936
+ "",
937
+ "const users = table({",
938
+ ' name: "users",',
939
+ " auth: true, // backs authentication",
940
+ " // `id` (int PK) + `created_at` (epochms) are auto-injected \u2014 don't declare them.",
941
+ " schema: {",
942
+ " email: f.email({ required: true }),",
943
+ " name: f.text(),",
944
+ " },",
945
+ ' // Indexes: { type, fields: [{ name, op? }] }. `"unique"` is shorthand for `"btree|unique"`.',
946
+ " // A DOTTED name indexes a key inside a column (`xdo.email`); only the part before the",
947
+ " // first dot is checked against the schema.",
948
+ ' index: [{ type: "unique", fields: [{ name: "email" }] }], // also: btree, hash, gin, search, gist, vector',
949
+ "});",
950
+ "",
951
+ "const posts = table({",
952
+ ' name: "posts",',
953
+ " schema: {",
954
+ " author: f.tableRef(users), // foreign key \u2192 users (NOT `ref`)",
955
+ " body: f.text({ required: true }),",
956
+ " },",
957
+ "});",
958
+ "",
959
+ 'const api = apiGroup({ name: "blog", canonical: "blog" }); // canonical \u2192 the URL token',
960
+ "",
961
+ "const createPost = query({",
962
+ ' name: "create_post", verb: "POST", apiGroup: api, auth: users, // the auth table',
963
+ " input: { body: input.text({ required: true }) },",
964
+ " stack: [",
965
+ ' s.db.add({ table: posts, row: { author: auth("id"), body: inp("body") }, as: "post" }),',
966
+ " ],",
967
+ ' response: ref("post"),',
968
+ "});",
969
+ "",
970
+ "const listPosts = query({",
971
+ ' name: "list_posts", verb: "GET", apiGroup: api,',
972
+ ' stack: [s.db.query({ table: posts, sort: [{ sortBy: "created_at", dir: "desc" }], as: "rows" })],',
973
+ ' response: ref("rows"),',
974
+ "});",
975
+ "",
976
+ 'export default workspace("my-blog")',
977
+ " .registerTables([users, posts])",
978
+ " .registerApiGroups([api])",
979
+ " .registerQueries([createPost, listPosts]);",
980
+ "```",
981
+ "",
982
+ "Compile: `xanosdk export ./index.ts --out bundle.json` (or `writeBundle(app, path)`",
983
+ "from `@xano/sdk/node` in code). The default export must be the `Xano` registry. The entry must be an",
984
+ 'ES module (Xano SDK defs are ESM-only): set `"type": "module"` in the nearest',
985
+ 'package.json or name the entry `.mts`. `npm init -y` writes `"type": "commonjs"`,',
986
+ 'which fails with a "must be ES modules" error until you switch it to module.',
987
+ "",
988
+ "Set `canonical` on every `apiGroup`. The engine mints the URL token server-side, so",
989
+ "without one a group's client paths are unresolvable until a lock exists: the bundle",
990
+ "exports fine and `xanosdk routes` / `getPath()` then fail on the very queries it just",
991
+ "built. An explicit `canonical` resolves them from the source alone.",
992
+ "\u26A0 A canonical is unique across the whole INSTANCE, not per workspace \u2014 two",
993
+ "workspaces cannot serve the same one, and no flag shares it. One set in code is a PIN:",
994
+ "a merge refuses (exit 2) when another workspace holds it. One the lock minted is a",
995
+ "preference the instance may keep or replace with a random token, and `--static` is",
996
+ "refused when it does.",
997
+ "",
998
+ "Build warnings: `export()` prints the shapes that deploy clean and then do the wrong",
999
+ "thing (a `bulk.update` zero-filling omitted columns, an `ignoreEmpty` on an empty",
1000
+ "operand, a `ref()` nothing binds). Each has a legitimate use, so each only warns \u2014",
1001
+ "but nothing fails on a message no one reads, so in CI and in unattended agent builds",
1002
+ "pass `--strict` (`emitBundle(app, { strict: true })` / `app.export({ strict: true })`):",
1003
+ "every warning becomes a hard failure. Same bundle bytes either way.",
1004
+ "Before calling the work done: `npm run typecheck`, `xanosdk export <entry> --strict`,",
1005
+ "`xanosdk deploy <entry>`, then `xanosdk test run-all` against what deployed.",
1006
+ "",
1007
+ "Identity: object guids derive from `(type, name)` \u2014 a query's from `(api group,",
1008
+ "verb, name)` \u2014 so renames change identity.",
1009
+ "EVERY build freezes every guid + api-group/toolset canonical in a lock file",
1010
+ "BESIDE THE ENTRY FILE \u2014 `xano/xano.lock` for the standard scaffold,",
1011
+ "NOT the project root \u2014 which you COMMIT (`--no-lock` opts out; CI guard",
1012
+ "`--frozen-lock` fails instead of changing it).",
1013
+ "To rename an object: rename in code, export (stderr prints the exact fix-up), run",
1014
+ "`xanosdk lock rename <kind> <old> <new>`, export again \u2014 the original guid is emitted",
1015
+ "under the new name, so the engine renames in place instead of delete+create. Taking",
1016
+ "over an existing workspace: `xanosdk lock import <live-bundle.json>` first, then",
1017
+ "export. Pruning, programmatic seeding, and which commands write the lock:",
1018
+ "`llms/lock.md`.",
1019
+ "",
1020
+ "## Deploy",
1021
+ "",
1022
+ "`xanosdk init` \u2192 `xanosdk deploy` \u2192 URL. A deploy runs the same compile pipeline as",
1023
+ "`export` (honoring `xano.lock`), imports the result, and prints the URL.",
1024
+ "\u26A0 Every deploy is a FULL REPLACE of its destination \u2014 objects AND records.",
1025
+ "",
1026
+ "**Backends** \u2014 one grammar: `workspace` | `ephemeral[:<name>]` | `local-engine[:<name>]`",
1027
+ "(no credential) | `tenant:<name>` | `release:<name>` | a bundle path where a file fits. Reads",
1028
+ "`--from`, writes `--to`, `test --on`; positional on `deploy`/`pull`/`generate`/`tables`/",
1029
+ "`impersonate`. Omitted = the ephemeral or local engine this project last deployed to, never",
1030
+ "workspace/tenant; bare `deploy` compiles the project onto an ephemeral instead (`--local-engine`:",
1031
+ "an engine on this machine), and `generate`/`init --from` need one. A refusal, typed or tracked,",
1032
+ "names the reason and the spellings (`publish`, `release create` refuse a local engine).",
1033
+ "`release create --from` records a run: no bundle path, no `release:`,",
1034
+ "and a `tenant:` only if throwaway (`standard`/`run` refused).",
1035
+ "",
1036
+ "**Destinations** \u2014 ephemeral by default, real only when you say so:",
1037
+ "",
1038
+ "- `xanosdk deploy [source]` \u2192 a NAMED, auto-expiring ephemeral (~1h; `--expires-hours`",
1039
+ " 1\u201324), tracked in `./.xano/ephemeral.json`, so deploying again REFRESHES it at the same",
1040
+ " URL; if it expired or was swept, a fresh one is created and the new URL called out.",
1041
+ " \u26A0 Only the BACKEND URL survives a refresh \u2014 the replace clears static hosting, so",
1042
+ " `--static` publishes a NEW host every run and the previous URL stops serving",
1043
+ " (not under `--keep-data`, which MERGES an env a deploy filled: rows kept, no re-seed,",
1044
+ " removed objects deleted; \u26A0 a rename loses values, a RETYPE reads unfit ones as null, env",
1045
+ " VALUES are not applied, a table trigger refuses it \u2014 `--reset` replaces and re-seeds).",
1046
+ "- `xanosdk release create <name>` cuts from a running source and LANDS in your workspace",
1047
+ " \u2014 the record that it came up, and the only thing `promote`/`tenant deploy` take. A",
1048
+ " taken name is refused, and so is one that addresses a LOCATION instead of naming: `/`,",
1049
+ " `\\`, `:`, or a leading `.`. Every export derives its FILENAME from the name it is given,",
1050
+ " so `release|ephemeral|workspace export` refuse those same shapes unless `--path` names",
1051
+ " the file. Carries EVERY table's SCHEMA either way; `--seed[=<guids>]` adds",
1052
+ " ROWS \u2014 guids from `xanosdk tables <backend>`; a redeploy KEEPS them.",
1053
+ "- `xanosdk release transfer <name> --to-profile <profile>` copies a release to another",
1054
+ " workspace/instance by CONTENT (tar sha256): identical \u2192 reused; same name, other",
1055
+ " bytes \u2192 exit 2.",
1056
+ "- `xanosdk promote <release>` / `xanosdk tenant deploy <tenant> <release>` land it.",
1057
+ " `promote` adds a branch; \u26A0 its TABLE edits hit live",
1058
+ " (`--allow-shared-schema-changes`). `tenant deploy` REPLACES: DROPS tables it lacks,",
1059
+ " CONVERTS retypes (`--seed`: rows).",
1060
+ " `promote` refuses a taken name/pinned slug (`SDK_IDENTITY_CONFLICT`), then",
1061
+ " EXITS NON-ZERO if a declared object did not arrive or a pinned slug is not served;",
1062
+ " `--set-live` waits on that (previous branch serving). Extra objects: reported;",
1063
+ " a check missing an input reports `unverified`, exit 0. `--json` on every release write:",
1064
+ " `completed` yes|no|unknown; UNKNOWN exits 9 \u2014 run `resolveWith`, never retry blind.",
1065
+ " `--expect-live <label>` (with `--set-live`) refuses if live moved; not atomic. Inspect a staged branch with",
1066
+ " `workspace export --branch <label>` or `workspace diff <entry> --branch <label>`.",
1067
+ " \u26A0 The redis family, `s.lambda`, `s.db.direct_query`, `s.await`, `s.microservice.request`,",
1068
+ " `s.zip.*`, the raw-file readers, Datadog and Webflow need an INSTANCE CAPABILITY: they",
1069
+ " deploy clean and fail on the first request if the target lacks it. `export`/`deploy` list",
1070
+ " what the bundle needs \u2014 compare it to the target yourself.",
1071
+ "- Backend SECRETS stay out of the repo: declare the NAME, value empty, in `workspaceConfig({ env })`",
1072
+ " passed to `registerWorkspace()`; the VALUE lives in `xano/.env` \u2014 gitignored, kept across a",
1073
+ " pull, read by every build. `--backend-env-file <path>` replaces that default (CI has",
1074
+ " no `xano/.env`), `--env-var KEY=VALUE` beats both, and `xanosdk env pull` fills the file from",
1075
+ " a running backend \u2014 the only command that writes `xano/.env`.",
1076
+ " \u26A0 A deploy REPLACES the env set: a declared name with NO value refuses the deploy rather",
1077
+ " than clearing the live one \u2014 `--allow-empty-env=NAME` clears it on purpose.",
1078
+ " ONE live value, no deploy: `xanosdk env set NAME` (value on stdin) / `env unset NAME`.",
1079
+ " (`--static-env` is the other one \u2014 PUBLIC frontend config.)",
1080
+ "- A DOCUMENTATION token is a secret too, in a SECOND ignored file: source declares only the",
1081
+ " gate \u2014 `documentation: { require_token: true }` on `workspaceConfig` or an `apiGroup` \u2014 and",
1082
+ " the value lives in `xano/.secrets.json`, keyed by the object it gates. A literal `token`",
1083
+ " FAILS the export. `xanosdk pull` writes that file and every build reads it back;",
1084
+ " `xanosdk secrets fill` MINTS one per gate that has none \u2014 a gate you AUTHORED has no value",
1085
+ " to pull. CI, which does not have the file, passes `--secrets-file <path>` or",
1086
+ ' `--doc-token "<scope>=<value>"`',
1087
+ " (scope = `workspace`, or the group's NAME).",
1088
+ " \u26A0 OMISSION IS OPPOSITE PER SCOPE: off `workspaceConfig` the target's gate is left alone;",
1089
+ " off an `apiGroup` it is CLEARED (an absent key there is written as the engine default). An",
1090
+ " unsupplied gate refuses a `deploy` and fails the export for a publishing group.",
1091
+ "- `xanosdk deploy --to workspace|tenant:<name>` skips the release flow to merge a LOCAL",
1092
+ " build in, keeping the checks that need one (`--dry-run`, `--prune`, loss warnings) and",
1093
+ " leaving nothing to roll back to. Prefer the release path.",
1094
+ " \u26A0 Branches scope LOGIC: tables and microservices are shared by EVERY branch, so a",
1095
+ " schema change reaches production on any branch \u2014 refused without",
1096
+ " `--allow-shared-schema-changes` \u2014 and dropping a column destroys its data.",
1097
+ " \u26A0 `--replace` CLEARS BY WORKSPACE, NOT BY BRANCH: every non-live branch goes, with the",
1098
+ " saved versions that would restore one \u2014 unrecoverable. Refused without",
1099
+ " `--allow-branch-deletion`; `--yes` does not imply it. The default merge leaves them alone.",
1100
+ "- `xanosdk workspace reset-tables <entry> --table <guid>` puts NAMED tables back to their",
1101
+ " compiled `table({ seed })` rows, touching no other table. Dry run unless `--write`.",
1102
+ " \u26A0 ROWS BEFORE LOGIC: rows are workspace-shared like schema, so they reach production when",
1103
+ " applied even while the logic reading them is staged \u2014 no ordering makes a half-data change",
1104
+ " atomic. Reset rows FIRST, then `promote`: new rows under old logic break only a feature",
1105
+ " being removed; new logic over old rows breaks the one you are shipping.",
1106
+ "- `xanosdk pull` refreshes THIS project from a backend: rewrites decoded files,",
1107
+ " keeps yours, lists and confirms (`--yes`).",
1108
+ "- `xanosdk generate <source>`: the decoded tree ONLY, no project.",
1109
+ "- `xanosdk status` names the env this project last deployed to, its URL and its expiry.",
1110
+ "",
1111
+ "**Frontend wiring.** `--static <dir>` (or `xanosdk publish <dir>`) injects the env's backend URL as",
1112
+ "`window.XANO_HOST` into EVERY html document in the build, before the app bundle runs,",
1113
+ "so the frontend needs no rebuild to target an env. How to read it, local dev, verifying",
1114
+ "the inject, and displaying a stored file: `llms/client.md`.",
1115
+ "\u26A0 A static host serves its files verbatim, so everything injected is PUBLIC \u2014 base",
1116
+ "URLs and publishable keys only, never secrets. Secrets go in backend env, read",
1117
+ "server-side via `env(name)`.",
1118
+ "\u26A0 `xanosdk preflight` uses the deploy credential unless `XANO_VALIDATE_INSTANCE` /",
1119
+ "`XANO_VALIDATE_TOKEN` (+ optional `XANO_VALIDATE_WORKSPACE_ID`) are set.",
1120
+ "",
1121
+ "**Full CLI surface:** `xanosdk <command> --help` \u2014 every command, flag, default and accepted",
1122
+ "backend; `manifest.json` carries the same in its `cli` array.",
1123
+ "**Style:** reach statements through the `s` namespace (`s.db.add`, `s.math.add`, \u2026).",
1124
+ "The flat aliases (`dbAdd`, `setVar`, \u2026) are identical in output; prefer `s.*`.",
1125
+ ""
1126
+ );
1127
+ lines.push(
1128
+ "## Gotchas",
1129
+ "",
1130
+ "Non-obvious authoring rules:",
1131
+ "",
1132
+ "### Values and references",
1133
+ "",
1134
+ "- **No callback builder.** Flat def-objects + `register*`, not",
1135
+ " `workspace(w => w.table(...))`. `workspace(name)` returns a named `new Xano()`;",
1136
+ " tables are `table({ schema: { col: f.text() } })`.",
1137
+ "- **Reference-helper picker:** `ref` = stack var (`as:` output), `inp` = input,",
1138
+ ' `col` = table column (in `db.query` `where`), `auth("id")` = the caller,',
1139
+ " `c.*` = a constant. Pick by what you're pointing at.",
1140
+ "- **Foreign key is `f.tableRef(table)`, not `ref`.** `ref(name)` references a",
1141
+ " stack variable (a value); `f.tableRef` is the column constructor.",
1142
+ "- **Tagged values are DATA \u2014 a JS template literal cannot compose them.** `${ref(...)}`",
1143
+ ' (or `"a" + ref(...)`) stringifies the tag object at BUILD time: ``c.text(`Hi ${ref("u.name")}`)``',
1144
+ " type-checks and encodes the literal text `Hi [object Object]`, served verbatim.",
1145
+ " `export()` warns; `--strict` fails. Compose at RUNTIME:",
1146
+ ' `withFilters(c.text("Hi "), fl.concat(ref("u.name")))`, an `obj({...})`/record member,',
1147
+ " or `c.expression('\"Hi, \" ~ $var.u.name')`.",
1148
+ '- **Declare inputs with `input.<type>()`, read them with `inp("name")`.** `inp()`',
1149
+ " resolves ONLY against this def's own `input` block \u2014 a value produced earlier in",
1150
+ ' the stack is `ref("var.field")`, not `inp("field")`. An undeclared name fails at',
1151
+ " runtime with `Unable to locate input` (`export()` warns).",
1152
+ "- **`get_input`/`get_raw_input` read the whole payload**, not one named input",
1153
+ " (args are `{ as?, encoding?, excludeMiddleware? }` \u2014 no `name`). For a single",
1154
+ ' input use `inp("name")`.',
1155
+ "- **To match every row, omit `where` \u2014 never pass a constant.** `where: c.bool(true)` is",
1156
+ ' not "no filter": a bare scalar literal states no condition and the engine reads the',
1157
+ ' resulting `context.search` as garbage, so it throws. An absent `where` is how "every',
1158
+ ' row" is spelled. A raw `Value` `where` stays the escape hatch for a clause built',
1159
+ ' elsewhere \u2014 `inp("clause")`, `ref("built_where")`, or `c.text("id > 0")`.',
1160
+ "",
1161
+ "### Tables and columns",
1162
+ "",
1163
+ "- **System columns are auto-injected.** `id` + `created_at` are prepended to",
1164
+ " every table (`system: true` by default); declaring them by hand is redundant.",
1165
+ ' `id` is an `int` PK by default; pass `idType: "uuid"` on the table for a uuid key.',
1166
+ " Both are valid targets wherever a column name is accepted \u2014 `db.query` `sort`/",
1167
+ " `output`, a `db.get`/`edit`/`del` `fieldName`, etc. (the column-name type is",
1168
+ ' `keyof schema | "id" | "created_at"`), and both appear in `InferRow<typeof table>`.',
1169
+ "- **A column named `run` is reserved.** The table deploys and reads back fine, then",
1170
+ " EVERY `s.db.add` into it 400s \u2014 at any column type, with or without a value \u2014 and",
1171
+ " the error names the column while complaining about the VALUE. Use `run_id`. Exact,",
1172
+ " case-sensitive, one name: `Run`/`runs`/`run_id` are fine. `--strict` fails on it.",
1173
+ "- **An input named `run` is reserved when a call passes it**: the callee gets",
1174
+ " defaults or a 400. HTTP binds it fine. `--strict` fails on it.",
1175
+ "- **Numeric columns lose money quietly.** A normalized table's `f.decimal` reads",
1176
+ " `0.12345678` back as `0.12346`; `f.int` saturates at `9223372036854775807`",
1177
+ " instead of failing. Both at HTTP 200 and plausible on read, so it surfaces as",
1178
+ " arithmetic that stops reconciling. Hold money as a count of the smallest unit;",
1179
+ " exact bounds: `llms/fields.md`.",
1180
+ "- **Self-referencing tables** need the bare-name form: inside `tweets`'s own",
1181
+ ' schema, write `f.tableRef("tweets", { type: "int" })` \u2014 the `const tweets`',
1182
+ ` handle isn't assigned yet, so the handle form throws "used before declaration".`,
1183
+ "- **Seed a table's starting rows with `table({ seed })`.** Rows typed against the",
1184
+ ' schema, inline (`seed: [{ name: "\u2026" }]`) or from a file (`seedFile("./seed.json",',
1185
+ " import.meta.url)`). Only `deploy` ships them, and a re-deploy re-seeds cleanly. Never",
1186
+ " put secrets in `seed`. Row shape, `id` pinning, and the file rules: `llms/fields.md`.",
1187
+ '- **`f.password()` defaults to `access: "internal"`, so `db.get` does NOT return it.**',
1188
+ ' A login stack that reads `ref("u.password")` after a plain `db.get` fails at runtime',
1189
+ " with `Unable to locate var: u.password` \u2014 the column is simply absent from the row.",
1190
+ ' Name it in the read\'s `output` to pull it: `s.db.get({ table: users, fieldName: "email",',
1191
+ ' fieldValue: inp("email"), output: ["id", "email", "password"], as: "u" })`, then',
1192
+ " `s.security.check_password`. `output` OVERRIDES column visibility \u2014 it is the only way to",
1193
+ " read an `internal` column, and `export()` warns when a stack reads one a `db.get` did not",
1194
+ " return.",
1195
+ "- **Don't take a password through `input.password` on login \u2014 it double-hashes.**",
1196
+ " An `f.password()` column hashes on write, and `input.password` *also* hashes the",
1197
+ " submission on bind, so `s.security.check_password` compares two different hashes",
1198
+ " and a correct password always fails (`ok:false` on a found row). Take the submitted",
1199
+ " password as `input.text()` on both signup and login and pass the plaintext straight",
1200
+ " to `check_password` (which does the comparison hash itself). `export()` warns.",
1201
+ "",
1202
+ "### Reading and writing rows",
1203
+ "",
1204
+ "- **DB reads are field-match, not `where`-expr.** `db.get`/`db.edit`/`db.del`/",
1205
+ " `db.has`/`db.patch` match one field: `{ fieldName, fieldValue }` (`fieldName`",
1206
+ " defaults to the PK `id`). Only `db.query` takes a `where`/`additionalWhere`",
1207
+ " `expr(...)`; writes (`db.add`/`db.edit`/`db.add_or_edit`) take a `row`/`data`.",
1208
+ " Signatures: `llms/statements-data.md`.",
1209
+ "- **Single-field only \u2014 no composite match.** These ops match exactly ONE field;",
1210
+ " there is no two-field form (the engine's by-field lookup takes a single",
1211
+ " predicate). For a `(a, b)` existence/fetch \u2014 e.g. dedupe a `(habit, date)`",
1212
+ ' check-in \u2014 use `db.query({ where: [expr(col("habit"), "=", ...), expr(col("date"), "=", ...)], as })`',
1213
+ " (a `where` array is ANDed) and branch on the result, rather than pushing the",
1214
+ " check to the client.",
1215
+ "- **Drilling into a maybe-null `db.get` result 500s \u2014 use `ref(path, { safe: true })`.**",
1216
+ ' `db.get` binds `null` on a no-match, but a nested `ref("owner.user_id")` raises a runtime',
1217
+ ' "Unable to locate var" (HTTP 500) when `owner` is null \u2014 an ownership guard throws instead',
1218
+ " of failing cleanly. Two correct shapes:",
1219
+ ' - Guard existence first \u2014 `s.precondition({ expr: expr(ref("owner"), "!=", c.null()),',
1220
+ ' error_type: "notfound", error: c.text("Not found.") })` \u2014 then drill WITHOUT `safe`.',
1221
+ " - Or drill null-safe inside an EXPRESSION or `obj()` operand:",
1222
+ ' `expr(ref("owner.user_id", { safe: true }), "=", auth("id"))` yields `null` (the guard',
1223
+ " reads `false`) instead of 500ing. Works inside `obj({...})` too \u2014 no `s.set_var` hoist needed.",
1224
+ " \u26A0 EXPRESSION and `obj()` operands ONLY \u2014 never a `db.*` match argument. `null` is not a",
1225
+ " legal `fieldValue`/`id`, so a chained get (fetch the child row, then its parent to check",
1226
+ " the owner) fails with HTTP 400 `Missing param: field_value` one statement BEFORE the guard.",
1227
+ " There guarding existence first is MANDATORY. `export()` warns on a safe ref in that position.",
1228
+ " \u26A0 That is for a base that EXISTS and may be null. If the base binds nothing at all (a typo),",
1229
+ " `{ safe: true }` buries the bug as a silent `null` \u2014 `export()` warns with a did-you-mean;",
1230
+ " fix the name instead.",
1231
+ "- **`output` selects from what the statement BINDS, and a PAGED `db.query` binds the",
1232
+ " envelope.** With `metadata: true` (the DEFAULT), the roots are `items.<column>` plus the",
1233
+ ' counters you want kept: `output: ["itemsReceived", "curPage", "items.id", "items.title"]`.',
1234
+ " A bare column list there matches no envelope key, so ALL of them are dropped and the",
1235
+ " endpoint answers `[]` at HTTP 200 with no error \u2014 and a `response` reading",
1236
+ ' `ref("rows.items")` then dies with `Unable to locate var: rows.items`. Prefix the columns,',
1237
+ " or set `metadata: false` inside `paging` to select the rows directly.",
1238
+ " `export()` warns; `--strict` fails.",
1239
+ "",
1240
+ "### Stacks and calls",
1241
+ "",
1242
+ "- **Block specials nest a `body`, not a `stack`.** `s.for`/`s.foreach`/",
1243
+ " `s.while`/`s.switch`/`s.try_catch`/`s.db.transaction`/`s.expect.to_throw`",
1244
+ " take their sub-stack as `body` (`try`/`catch`/`finally` for `try_catch`);",
1245
+ " `s.group(body)` and `s.util.post_process(body)` take it **positionally**.",
1246
+ " `s.for` is **count-bounded** (`{ as, count: <Value>, body }`), not from/to. See the",
1247
+ " authored signatures in `llms/statements-data.md`.",
1248
+ "- **`s.api.call` / `s.task.call` / `s.trigger.call` / `s.workflow_test.call` are",
1249
+ " WORKFLOW-TEST ONLY.** Outside a `workflowTest({...})` stack the engine cannot reach",
1250
+ " the target, so one in a query/function/task deploys clean and then answers the first",
1251
+ " real request with `ERROR_FATAL: <Type> does not exist: <type>:<n>` \u2014 and not per",
1252
+ " host kind, the same call fails identically from a function a query runs.",
1253
+ " `export()` refuses them. `s.function.call`, `s.tool.call`, `s.middleware.call` and",
1254
+ " `s.addon.call` run anywhere, as does `s.function.run` (the ordinary function",
1255
+ " invocation). To share logic between endpoints, put it in a `defineFunction` and",
1256
+ " `s.function.run` it from both.",
1257
+ "- **`expect.*` is a unit-test assertion; `s.expect.*` is a workflow-test statement.**",
1258
+ " A statement's `mock` is keyed by TEST NAME; an undeclared name throws. See `llms/tests.md`.",
1259
+ "- **A helper returning `Statement[]` widens the stack and kills `InferResponse`.**",
1260
+ " Spreading `...requireX()` where the helper is typed `Statement[]` drops the stack's TUPLE",
1261
+ " type, so EVERY `ref()`/`as` in that stack \u2014 even ones after the spread \u2014 resolves to",
1262
+ " `unknown` and the response types as `StackTupleWidened`. Nothing fails at the helper; it",
1263
+ " surfaces where the response is consumed, often a frontend typecheck. Fix: return",
1264
+ " `statements(s.a(...), s.b(...))` \u2014 its tuple survives the spread. A helper that builds",
1265
+ " its array in a LOOP cannot be a tuple; declare `responseShape` on the query there.",
1266
+ "- **Build regex-filter patterns with `c.regex(body, flags?)`, never `c.text`.**",
1267
+ " The regex filters (`regex_test`/`regex_match`/`regex_replace`/\u2026) are PHP `preg_*`: the",
1268
+ " piped value is the PATTERN and must be delimiter-wrapped, and the ARGUMENT is the subject.",
1269
+ ' Correct: `withFilters(c.regex(/^[a-z-]+$/i), fl.regex_test(inp("slug")))`.',
1270
+ ' - A bare `c.text("^\u2026$")` is an invalid pattern that matches *nothing* for every input,',
1271
+ ' so a precondition on it silently rejects all values. `c.regex(body, "i")` wraps +',
1272
+ " escapes it for you (a JS `RegExp` too: `c.regex(/^\u2026$/i)`).",
1273
+ " - Reversed \u2014 subject piped, pattern in the argument \u2014 reads correctly, type-checks, and",
1274
+ " answers false for EVERY input, so an `if (matches) reject` guard admits what it refuses.",
1275
+ " - Build time refuses both: `withFilters` throws on a bare `c.text` pattern from ANY position",
1276
+ " in the chain (a normalizer in front of the regex filter is refused too; nothing upstream",
1277
+ " adds the delimiters) and on a pattern found in the subject slot; `s.expect.to_match` is the",
1278
+ " same PATTERN slot, refused both ways; a `ref`/`inp` pattern is passed through untouched.",
1279
+ " `export()` warns on a reversed pair in stored bytes.",
1280
+ " - Better still: a native typed input (`input.email`) over hand-rolled validation.",
1281
+ "- **An `auth()`-keyed limiter FAILS the request on any host with no caller identity \u2014**",
1282
+ ' **at every tier.** `s.redis.ratelimit({ key: withFilters(c.text("rl:"), fl.concat(auth("id"))), error: c.text("\u2026") })`',
1283
+ " is the per-user form, and a public query, a task, or a function has no caller identity.",
1284
+ " It does not degrade to one shared bucket: on a public endpoint the FIRST call 403s,",
1285
+ " under `max`, and the host never runs. Key off `sys.remoteIp()` there. Attaching it",
1286
+ " at `apiGroup({ middleware })` or the workspace tier inherits the same failure onto",
1287
+ " every member endpoint. Export warns, naming the tier.",
1288
+ "",
1289
+ "### Identity, build, and deploy",
1290
+ "",
1291
+ "- **Same-name siblings collide \u2014 queries excepted: their identity carries group + verb.**",
1292
+ " Guids derive from `(type, name)`, so two functions (or tables, toolsets, \u2026) sharing",
1293
+ " a name derive ONE guid and `export()` throws \u2014 give them DISTINCT names. A QUERY",
1294
+ " derives from `(api group, verb, name)`, the engine's own uniqueness: `GET items` +",
1295
+ " `POST items`, and `items` across two groups, coexist, and the lock keys the same",
1296
+ " composed identity (`query:blog|GET|items`) \u2014 changing a query's verb or group",
1297
+ " changes its identity. An explicit `guid` on a colliding pair clears the throw but",
1298
+ " is NOT a lasting fix: a lock entry cannot hold two guids, so `export --lock`",
1299
+ " refuses the pair (`export()` warns even unlocked); it is for pinning identity",
1300
+ " across a rename, not for sharing a name.",
1301
+ "- **`export()` vs `emitBundle()` vs `writeBundle()`:** `export()` returns the object,",
1302
+ " `emitBundle()` the JSON, `writeBundle(app, path)` writes `xanosdk export`'s file. All",
1303
+ " three run the SAME build-time checks, INCLUDING seed validation of a literal `seed: [...]`",
1304
+ " array (row shape, unknown column, coercion, enum membership, the all-or-nothing `id`",
1305
+ ' rule). A DEFERRED seed \u2014 a thunk (`() => import("./seed.json")`) or `seedFile()` \u2014 is',
1306
+ " checked only by the `xanosdk export`/`deploy` CLI path. Only `deploy` ships seed rows.",
1307
+ " - Entry points: the `node:fs` writers (`writeBundle`/`writeArtifact`) and lock-file I/O",
1308
+ " import from `@xano/sdk/node`, NOT the browser-safe `@xano/sdk` entry (a frontend",
1309
+ " imports defs from it for `getPath()`/`InferInput`). `@xano/sdk/internal` is the",
1310
+ " compiler machinery, never needed to author. READING a bundle back is `@xano/sdk/bundle`",
1311
+ " \u2014 a statement walker (`2.if.0` paths), a structural hash, `mvp:*` catalog, `tableRefOf`.",
1312
+ "- **Intra-workspace imports use `.js` specifiers** (`../tables/links.js`), not",
1313
+ " extensionless \u2014 the defs compile under `moduleResolution: bundler`. Add the `.js`.",
1314
+ "- **Agents authenticate with env vars \u2014 never `xanosdk login`.** `login` blocks on a",
1315
+ " browser consent no agent can complete. Set all three of `$XANO_INSTANCE_URL`,",
1316
+ " `$XANO_WORKSPACE_ID`, `$XANO_META_TOKEN` and run `deploy`/`promote` directly: no disk, no",
1317
+ " rotation, so it survives repeated runs.",
1318
+ " - The triple outranks every other credential (`--config`, `$XANO_PROFILE` and",
1319
+ " `$XANO_REFRESH_TOKEN` included, each named on stderr when displaced). Setting SOME of the",
1320
+ " three is a hard error, so a misspelled secret cannot fall through to whatever credential",
1321
+ " the runner has. An explicit `--profile` against it is a hard error too, not a displacement.",
1322
+ " - On disk: `xanosdk profile add <name> --instance \u2026 --workspace-id <n>` reads the token",
1323
+ " from piped stdin, else PROMPTS (a flag would reach the process list); the instance must accept it.",
1324
+ " `auth.json` holds a map of named profiles, selected by `--profile <name>`. The older",
1325
+ " `$XANO_REFRESH_TOKEN` + `$XANO_CLIENT_ID` pair still works but ROTATES: single-use.",
1326
+ "- **Event-driven objects fire on an EPHEMERAL.** A `task` (scheduled), an `mcpServer`,",
1327
+ " and every trigger \u2014 `tableTrigger` included \u2014 run normally on an ephemeral env, which",
1328
+ " is where `deploy` sends them. So test an event-driven design (screen-on-insert, cron",
1329
+ " cleanup, MCP tool call) by deploying it and letting it run.",
1330
+ ""
1331
+ );
1332
+ const objectKinds = [];
1333
+ objectKinds.push("## Object kinds", "");
1334
+ objectKinds.push("Author with the factory, register on the Xano instance, lands under the payload key. Each line ends with a one-liner on what the primitive is.", "");
1335
+ objectKinds.push(
1336
+ "\u26A0 Composing a workspace from independently-authored modules: `register*` takes its defs however they arrive, but `modules.flatMap((m) => m.tables)` does NOT typecheck. `Array.prototype.flatMap` binds its element type to the FIRST element, so every later module's tables are checked against that one table's schema \u2014 the error compares two unrelated column names and mentions neither `flatMap` nor the cause. `.concat()` collapses the same way. Two spellings work: pass an array LITERAL (`registerTables([...a.tables, ...b.tables])` \u2014 spreads are fine, TypeScript infers the union across every element at once), or annotate the module array with the wide aliases `AnyTableDef` / `AnyQueryDef` / `AnyFunctionDef` / `AnyAddonDef`, exported for exactly the four def types whose generics can collapse this way. Widening the array costs nothing: the typing you need lives on the `table()`/`query()` handle you hold and pass to `s.db.*`, and is never read back off the registered array.",
1337
+ ""
1338
+ );
1339
+ for (const k of m.objectKinds) {
1340
+ if (k.subKinds && k.subKinds.length > 0) {
1341
+ for (const sub of k.subKinds) {
1342
+ if (sub.legacy) continue;
1343
+ objectKinds.push(
1344
+ `- ${k.kind} (${sub.objType}): \`${sub.authorFactory}\` \u2192 \`Xano.${k.registerMethod}\` \u2192 payload \`${k.payloadKey}\` \u2014 ${sub.description}`
1345
+ );
1346
+ }
1347
+ } else {
1348
+ objectKinds.push(`- ${k.kind}: \`${k.authorFactory}\` \u2192 \`Xano.${k.registerMethod}\` \u2192 payload \`${k.payloadKey}\` \u2014 ${k.description}`);
1349
+ }
1350
+ }
1351
+ objectKinds.push("");
1352
+ const kindsCore = [
1353
+ "## Object def shapes",
1354
+ "",
1355
+ "The def-object passed to each factory. `?` = optional. `input` is keyed by",
1356
+ "input name (`input.<type>(opts?)`); `stack` is `Statement[]` (`s.*`); `response`",
1357
+ "is a `ResponseDef` (see **Responses** below). Object identity is `guid?` \u2014",
1358
+ "omit it and it derives from `name` (for a query, from group + verb + name; set it",
1359
+ "to survive a rename).",
1360
+ "",
1361
+ "- `defineFunction({ name, guid?, description?, docs?, workspace?, input?, stack?, response?, tests? })`",
1362
+ "- `query({ name, verb, apiGroup?, guid?, auth?, input?, stack?, response?, responseType?, apiEnabled?, disabled?, cache?, description?, docs?, tests?, example? })` \u2014 a `cache` setting turns caching on unless `active: false`",
1363
+ ' - `verb`: `"GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD"` (required), UPPERCASE. Anything else \u2014 most often a lowercase `"post"` \u2014 makes `query()` THROW, because Xano does NOT reject it: it stores the verb as NULL, a null verb serves as GET, and the endpoint then answers on the wrong method while the one you meant 404s `Unable to locate request.`',
1364
+ " - `apiGroup`: an `apiGroup()` def handle (or its name) \u2014 binds by guid, stable across syncs. Raw numeric `apiGroupId?` is the escape hatch and wins if both given.",
1365
+ ' - `auth`: `false` (no auth) or an auth-table id; `responseType`: `"standard" | "stream"` (default `standard`) \u2014 any other spelling THROWS, since Xano stores an unrecognized one as NULL and a null buffers as `standard`, so a misspelled stream quietly does not stream.',
1366
+ " - `name` is the endpoint PATH within the group.",
1367
+ ' - A `{param}` segment is a URL PATH PARAM bound to the input of the same name, and segments chain: `name: "blog/{slug}/review/{review_id}"` + `input: { slug: input.text(), review_id: input.int() }`. Read it with `inp("slug")` like any other input.',
1368
+ " - Every `{param}` MUST have a matching input or `query()` THROWS \u2014 Xano treats an unbound marker as inert route text, so the endpoint would answer on the path and see nothing.",
1369
+ ' - A `{param}` need NOT be a whole segment (`"blog/post-{slug}"` routes fine), but its type must fit one segment (no object/list/json/file/geo/vector); there are no wildcards or patterns. `required: true` is NOT demanded (the engine\'s editor leaves path inputs unmarked).',
1370
+ ' - An input a `GET`/`DELETE`/`HEAD` looks ONE ROW up by (`s.db.get`/by-field edit/patch/delete; not `has` or an email/token/code) belongs in the path \u2014 `export()` warns `query.path-segment-candidate`: `getPath()` types STATIC. A segment is any value naming WHICH resource is wanted (`"shop/{country}"`); one that NARROWS A LIST (`s.db.query`) stays a query param. Intended? `diagnostics: { allow: [code] }` on the query (any def, for its own advisories) accepts it under `--strict`.',
1371
+ ' - Routes are matched FIRST-FIT in creation order, and a literal gets NO precedence over a `{param}`: `"runs/{id}"` (a text `id`) created before `"runs/trend"` answers `GET /runs/trend` itself. Two routes in one group and verb that one request path can match are REFUSED at `export()` (`query.route-shadowed`) \u2014 reordering `registerQueries` cannot fix it, since a release keeps an existing route first. Make the paths disjoint (`"runs/by-id/{id}"`), or declare a numeric param `input.int({ required: true })` (or `decimal`): only a REQUIRED int/decimal segment matches digits only \u2014 an OPTIONAL one matches any text, so `input.int()` still takes `trend`.',
1372
+ " - Inputs absent from the path are ordinary query-string/body params.",
1373
+ ' - Name charset is ONLY `A-Za-z0-9_-/{}`, max 200: a `.` (`"export.zip"`) is NOT rejected by Xano \u2014 it stores an EMPTY name that deploys clean then 404s forever, so `query()` THROWS. Use `"export_zip"` and set the extension in the response headers.',
1374
+ ' - **Client recipe:** `q.getPath({ params: { slug: "hello" } })` \u2192 `/api:<canonical>/blog/hello` \u2014 never interpolate by hand.',
1375
+ " - `getPath` percent-encodes each value (so `?`/`#`/spaces stay in their segment) and throws on what encoding cannot contain: a `/`, and a segment that becomes `.`/`..` as sent (a URL parser drops those before routing, addressing a different endpoint).",
1376
+ " - The keys are typed from the literal `name`, so a typo is a compile error.",
1377
+ " - `q.toSearchParams(input)` drops GET path params (free `query.toSearchParams` keeps them); lists send `k[]=a&k[]=b` (`k=a&k=b` binds only `b`).",
1378
+ " - Browser bundle cost, the `routes.gen.ts` alternative, and spot-checking a def from Node: `llms/client.md`.",
1379
+ "- `apiGroup({ name, guid?, canonical?, description?, docs?, swagger?, apiGroupEnabled?, documentation?, cors?, middleware? })` \u2014 a query container; queries bind to it via their `apiGroup`. `canonical` holds only letters, digits, `_` and `-` (it is the `<canonical>` of `/api:<canonical>`); anything else THROWS at build.",
1380
+ " - `documentation?`: `{ require_token? }` \u2014 the gate on this group's hosted docs. `require_token: true` IS the declaration; the token lives in `xano/.secrets.json` under this group's guid, and a literal `token` FAILS the export. \u26A0 ALWAYS emitted, present or not: an absent key is written as the engine default on import, so omitting the block CLEARS an existing gate (the opposite of the `workspaceConfig` rule). `swagger: true` publishes the docs; `require_token` gates them only alongside a non-empty token, so `swagger: true` with no gate makes the whole group readable by anyone with the URL \u2014 export warns once per build and `--strict` fails; `diagnostics: { allow: [\"api-group.docs-public\"] }` accepts it. A declared gate with NO token drops the block, so those bytes clear it: export FAILS under `swagger: true`, and with `swagger` off warns and completes.",
1381
+ " - `cors?`: `{ mode?, allowOrigins?: string[], allowHeaders?: string[], allowCredentials?, maxAge?, allowMethods?: { get?, post?, put?, patch?, delete?, head? } }`.",
1382
+ ' - `mode?`: `"default"` (the default) | `"custom"` | `"disabled"`, lowercase \u2014 a fourth value THROWS at export (`apiGroup()` itself does not check), because Xano neither rejects nor blanks it: it DROPS THE WHOLE API GROUP on import, so the deploy succeeds and every query in the group 404s. \u26A0 Every OTHER field applies only under `"custom"`: `"default"` serves a FIXED permissive policy (any origin, `allow-headers: *`, `allow-credentials: true`, `max-age: 86400`) and ignores the block, so setting `maxAge`/`allowCredentials`/`allowHeaders` alone changes nothing. `"disabled"` sends no CORS headers at all, so every browser call fails.',
1383
+ ' - \u26A0 Under `"custom"`, `allowOrigins` is matched as EXACT strings (scheme+host+port, no wildcard or subdomain expansion) and `"*"` is compared as a literal origin \u2014 it matches NOTHING. An unmatched origin gets no `access-control-*` headers at all, so the browser call fails while export, deploy and the preflight look fine. Name each origin, or use `mode: "default"` for any-origin. `allowMethods` gates the REAL response too: a verb left off gets no CORS headers back even though its preflight passes. Export warns on an empty origin list, a `"*"` entry, and a policy with no method enabled; a non-origin entry (trailing `/`, path) is refused.',
1384
+ "- Tasks, workflow tests, middleware and tools below (agents and MCP servers: `llms/kinds-agent-mcp.md`) share the envelope conventions (`guid?`, `description?`, `docs?`, `tags?`, `history?`) unless noted.",
1385
+ "- `task({ name, guid?, description?, docs?, datasource?, active?, tags?, history?, schedule?, stack?, middleware? })` \u2014 a scheduled background job (function-like `stack`, no `input`/`response`). `active?` defaults `true`; `active: false` deploys it parked.",
1386
+ ' - `schedule?`: a `ScheduleDef[]` (NOT a single object) \u2014 `{ startsOn, freq?, repeatEnabled?, endsOn?, endsEnabled? }`. `startsOn`/`endsOn` are **timestamp strings** validated at encode time \u2014 `"2026-01-01T00:00:00Z"`, or the space-separated `"2026-01-01 00:00:00+0000"` a pulled workspace carries \u2014 never epoch numbers, and never zoneless; `freq` is the repeat interval **in SECONDS** (omitted \u21D2 runs ONCE) \u2014 spell it `every("15m")`, a compile-time duration-to-seconds helper (`s`/`m`/`h`/`d`/`w`, terms concatenate). No cron, no timezone \u2014 a fixed-offset `startsOn` drifts an hour at each DST change; for a local time run hourly, gated on the local hour `withFilters(c.now(), fl.epochms_date("G", "America/New_York"))`. `repeatEnabled?`/`endsEnabled?` follow whether `freq`/`endsOn` is set and are recovery-only \u2014 state one to reproduce a stored gate left OFF. Fires on an ephemeral (see Gotchas).',
1387
+ '- `workflowTest({ name, guid?, description?, docs?, datasource?, active?, tags?, stack? })` \u2014 an end-to-end test. NO `input`/`response`: `.call` something with an `as`, then assert on that var \u2014 `s.function.call({ fn, input, as: "r" })`, `s.expect.to_equal({ expr: ref("r"), value: c.int(42) })`. `s.expect.*` belongs here \u2014 it is not inert elsewhere (a failure 500s the request), so treat one in a query/function/task as a mistake to remove. `active?` defaults `true`; chain tests with `s.workflow_test.call({ workflowTest: <def handle> })`.',
1388
+ ' - `datasource?`: Default `""` is an EMPTY datasource (recommended), not "no datasource". Any non-empty name makes the engine CLONE it before EVERY run \u2014 against production-sized data, slow enough to fail the run, which is why `"live"` warns at compile time. On an EPHEMERAL, `"live"` holds only the seed fixtures (plus rows a `--keep-data` deploy kept), so it is the way to read `table({ seed })` rows \u2014 a small clone, dropped after the run. \u26A0 The value is STORED, so clear it before you promote or the same test clones the real database.',
1389
+ "- `middleware({ name, guid?, description?, docs?, resultStrategy?, exceptionPolicy?, tags?, history?, input?, stack?, response?, responseShape?, tests? })` \u2014 a pre/post interceptor (function-like `stack`); attach it via a host's `middleware: { pre, post }`. \u26A0 `input` ENCODES but an ATTACHED middleware never has it bound \u2014 the host request binds its own inputs, so `inp()` inside pre/post fails at runtime with `Unable to locate input` and a declared default does not stand in (`export()` warns). Read the request body with `s.util.get_all_input` instead; it yields a `{ type, vars }` envelope whose `vars` DIFFERS BY PHASE: `pre` \u2192 the request inputs (`payload.vars.<field>`); `post` \u2192 `{ status, result }`, the host's outcome (`payload.vars.result.<field>`; plus `payload` on an error). A request field read in `post` 500s after the host already ran (`export()` warns); read the request there with `s.util.get_raw_input`. `s.middleware.call` is the one path that DOES bind the declared map.",
1390
+ ' - `resultStrategy?`: `"merge" | "replace"` (default `merge`) \u2014 how the middleware `response` folds into the accumulator of the phase it is ATTACHED to, and the two phases accumulate DIFFERENT things. In `post` the accumulator is the host\'s RESULT, so a returned object changes what the CALLER receives. In `pre` it is the host\'s REQUEST INPUTS, so a returned object changes what the HOST receives. `merge` folds key-by-key, `replace` substitutes wholesale; either way the next entry in the chain sees the updated value. So `pre` + `replace` DISCARDS every caller input the middleware does not re-emit, and a discarded input is then unreadable \u2014 a 500, even where it is declared `required: false`, because input defaulting has already happened by the time the override lands.',
1391
+ ' - `exceptionPolicy?`: `"silent" | "rethrow" | "critical"` (default `"rethrow"` \u2014 a throw ABORTS the request and surfaces the authored error/status, which is what a guard wants). `"silent"` swallows the throw and lets the request through, so a guard set to it is NOT enforced \u2014 use it only for advisory middleware. `"critical"` is `"rethrow"` plus skipping the `post` chain.',
1392
+ "- `tool({ name, guid?, description?, instructions?, docs?, enabled?, title?, annotations?, icons?, output?, tags?, history?, input?, stack?, response?, responseShape?, middleware? })` \u2014 a function-like operation (`input`/`stack`/`response`) that a toolset (MCP server or agent) exposes. Register it, then reference it from a toolset's `tools`. `title`/`annotations`/`icons`/`output` are MCP metadata (`llms/kinds-agent-mcp.md`).",
1393
+ '- An addon is a single table-bound db query, NOT a statement stack: `addon({ name, table, tableAlias?, where?, sort?, output: [cols], cardinality?: "single"|"list"|"count"|"exists"|"aggregate", group?, eval?, input?, context? })`, registered via `registerAddons([...])`.',
1394
+ " - `table` auto-fills the `context.dbo` binding. \u26A0 Never author `table: null` \u2014 that is a BROKEN table-less addon returning nothing; `codegen` emits it only for an already-broken pulled object.",
1395
+ ' - `tableAlias` is its SQL alias (`context.dbo.as`), qualifying `where`/`sort` columns (`col("merchant.id")`).',
1396
+ ' - `where`/`sort` take the same surface as `s.db.query`; `where` encodes `context.search`, `sort` encodes `context.return.list.sort` (`return.single.sort` with `cardinality: "single"`) and is refused with `count`/`exists`. `where` is the predicate binding the addon to the parent row \u2014 `expr(col("id"), "=", inp("user_id"))`.',
1397
+ ' - `cardinality` shapes the result (`context.return.type`, omitted for the `"list"` default). Rarer context (`eval`/`bind`/`lock`) stays raw `context` passthrough.',
1398
+ " - End to end \u2014 define, register, attach:",
1399
+ " ```ts",
1400
+ ' const author = addon({ name: "author", table: users, input: { user_id: input.int() }, where: expr(col("id"), "=", inp("user_id")), output: ["id", "display_name"], cardinality: "single" });',
1401
+ ' workspace("forum").registerAddons([author]); // plus the tables/queries',
1402
+ ' s.db.query({ table: threads, addon: [{ addon: author, as: "_author", input: { user_id: out("author_id") } }], as: "rows" }) // rows[]._author: { id, display_name }',
1403
+ " ```",
1404
+ "### Responses",
1405
+ "",
1406
+ "The `response?` field (on functions, queries, tools, middleware, and",
1407
+ "response-bearing triggers) maps to the stored `result[]`:",
1408
+ "",
1409
+ "- `ResponseDef = Value | Record<string, Value>`.",
1410
+ '- A single `Value` \u2192 one unnamed result item: `response: ref("rows")`.',
1411
+ '- A record \u2192 one named item per key: `response: { user: ref("u"), token: ref("t") }`.',
1412
+ "- Omitted \u2192 empty `result[]` (no response body).",
1413
+ "",
1414
+ "### Expressions (`expr`)",
1415
+ "",
1416
+ "`expr(left, op, right)` builds the comparison used by every condition/`where`",
1417
+ "surface \u2014 `s.conditional`/`s.while` `when` (incl. each `elif` branch), and",
1418
+ "`db.query` `where`/`additionalWhere` (and the search triggers) \u2014 one shared tree.",
1419
+ "",
1420
+ "- `op`: `=`, `!=`, `>`, `<`, `>=`, `<=` (JS aliases `==` `===` `!==` are accepted and normalized).",
1421
+ '- `left`/`right` are `Value`s \u2014 `col("x")` (a table column, `db.*` statements only), `ref`, `inp`, `auth(...)`, or `c.*`.',
1422
+ "- For the full operator set (`in`/`like`/`ilike`/`between`/`contains`/`overlaps`/`@>`/`~`/`search`/\u2026)",
1423
+ " use `cmp(left, op, right, { ignoreEmpty? })`; compose nested boolean logic with `and(...)`/`or(...)`.",
1424
+ "- \u26A0 The wider `cmp` operators are DATABASE-only (`where`, table view filter, db trigger",
1425
+ " `search`). A RUNTIME condition \u2014 `s.conditional`/`elif`, `s.while`, `s.precondition`,",
1426
+ " `array.*` `if` \u2014 takes the `expr` set only; the rest are refused at build time because",
1427
+ " deployed they fail the request with `Invalid op: <op>` on that branch, usually a guard.",
1428
+ " `cond.*` builds the filter-then-compare form that DOES run there, and handles the operand",
1429
+ " direction (the engine's `in` filter pipes the ARRAY, the reverse of `contains`): `cond.in`,",
1430
+ " `notIn`, `contains`/`icontains`/`notContains`, `startsWith`/`endsWith` (+ `i` forms),",
1431
+ " `empty`/`notEmpty`, `isNull`/`notNull`, `between(v, lo, hi)`, `has(obj, path)`, `count(v, n)`.",
1432
+ " Each returns a `Comparison`, usable anywhere `expr(...)` is.",
1433
+ "- A condition/`where` accepts a single `expr(...)`/`cmp(...)`, an `and()`/`or()` group, an array of",
1434
+ " those (ANDed), or (for `where`) a raw `Value`. `s.conditional`/`s.while`/`s.switch`, `db.query`,",
1435
+ " `precondition`, and the `array.*` predicates all take the same TREE shape (operators per above).",
1436
+ "- \u26A0 `mixed(a, { or: b }, { and: c })` reproduces a container whose terms do NOT all join the",
1437
+ " same way \u2014 pulled workspaces contain it. **Do not author it** (export warns `condition.mixed`). The",
1438
+ " stored form does not record the grouping, and the two places it can appear disagree: a",
1439
+ " branch (`s.conditional`/`s.while`/`precondition`) folds terms strictly left to right, so",
1440
+ " `a OR b AND c` is `(a OR b) AND c`, while a `db.query` filter applies the engine's",
1441
+ " AND-before-OR precedence and selects `a OR (b AND c)`. Write `and(or(a, b), c)` or",
1442
+ " `or(a, and(b, c))` \u2014 each says one reading in every context. Pulls report these as",
1443
+ " `ambiguous-condition`.",
1444
+ "- A **filtered** operand (`withFilters(...)`) works inline in any condition/`where` (conditional,",
1445
+ ' while, `db.query`/addon, \u2026) \u2014 e.g. `cmp(withFilters(col("title"), fl.trim()), "=", inp("q"))`.',
1446
+ "- **Compose a rule set as SIBLINGS, not a folded chain.** `and(...rules)` takes any",
1447
+ " number of terms and encodes flat; `rules.reduce((acc, r) => and(acc, r))` nests one",
1448
+ " container per rule, which costs quadratic bytes (512 terms: 394 KiB flat, 21 MiB",
1449
+ " folded) and is refused past 128 levels. Mixed joins: `and(or(...anyOf), ...allOf)`.",
1450
+ '- e.g. `db.query({ table: posts, where: expr(col("author"), "=", auth("id")), as: "rows" })`.',
1451
+ ""
1452
+ ];
1453
+ const kindsKnowledge = [
1454
+ "- `knowledge({ name, description?, type?, mode?, enabled?, body, refs?, guid?, tags? })` \u2014 markdown Xano's own AI reads before it acts \u2014 the workspace's builder agent, and outside agents reading the workspace through the Meta API or MCP. It does NOT reach an `agent()` run by `s.ai.agent.run`, whatever its `type` or `mode`: put what such an agent must know in its `llm.systemPrompt`. Takes `registerKnowledge`.",
1455
+ ' - `body` (REQUIRED): `knowledgeFile("./runbook.md", import.meta.url)` \u2014 a path to a real markdown FILE. There is no inline-string form. The path resolves relative to the MODULE THAT DECLARES the item, not the process working directory and not the workspace entry; `import.meta.url` is required and is what makes that true.',
1456
+ ' - `refs?`: `knowledgeDir("./runbook", import.meta.url)` \u2014 a directory whose WHOLE tree ships, recursively, paths preserved: UTF-8 text only (else refused; binary `.DS_Store` skipped), symlinks not followed. Keep the body OUT of it, or it ships as a reference file.',
1457
+ ' - `type?`: `"skill"` (default) | `"doc"` | `"agents.md"`. `skill` and `doc` differ in how they are filed, not in what an agent receives.',
1458
+ ' - `mode?`: `"auto"` (default) | `"always"` | `"referenced"`.',
1459
+ " - `enabled?` defaults `true`. A disabled item is stored and never reaches an agent.",
1460
+ "",
1461
+ "### What reaches the agent",
1462
+ "",
1463
+ '- `type: "agents.md"` \u2014 full body on EVERY turn, whatever `mode` says. At most ONE per workspace; a second is an export error. Setting `mode` on one warns, because nothing reads it.',
1464
+ '- `mode: "always"` \u2014 full body on every turn.',
1465
+ '- `mode: "referenced"` \u2014 full body only on turns whose message names the item (bare name or `@name`).',
1466
+ '- `mode: "auto"` \u2014 name and `description` only; the agent loads the body when a request matches.',
1467
+ '- Reference files are never injected wholesale \u2014 an agent searches them on demand. That is what makes a large `mode: "auto"` item cheap.',
1468
+ '- \u26A0 `mode: "always"` on a long body spends its whole length on every request, and nothing in the types says so. `"auto"` is the default for that reason. For an `auto` item `description` is ALL the agent sees until it decides to load the body, so write it to be matched against a request rather than as a title.',
1469
+ "- `name` takes letters, digits, and `/ _ - { } . ` or a space; anything else is an export error."
1470
+ ];
1471
+ const toolchain = [
1472
+ "**Toolchain modules** extend the CLI, not the workspace \u2014 nothing to register in",
1473
+ "`xano/index.ts`. An installed one runs on `export` and `deploy` (after the compile,",
1474
+ "before the upload) and is verified, never rewritten, under `--frozen-lock`. Config",
1475
+ 'lives in this project\'s `package.json` `"xanosdk"` block, keyed by package name.',
1476
+ "\u26A0 `marketplace install <pkg>` is not just `npm install`: it asks the module its questions",
1477
+ "and writes that block plus the `.gitattributes` lines it contributes. A module that",
1478
+ "arrived any other way (a plain `npm install`, a merged PR) is configured by running that",
1479
+ "same command on it; until then `export`/`deploy` report it unconfigured and it runs on",
1480
+ "shipped defaults. `marketplace reinstall <pkg>` re-asks with the current settings as the",
1481
+ "defaults and re-enables a disabled one; `marketplace remove <pkg>` drops block, lines and",
1482
+ "dependency together \u2014 a plain `npm uninstall` leaves the block, and a package it names",
1483
+ "that is no longer installed fails every `--frozen-lock` run."
1484
+ ];
1485
+ const client = [
1486
+ "Importing a def into a browser bundle or a Node script \u2014 for `getPath()`, `getUrl()`, `verb`, or `InferResponse` \u2014 runs its factory calls; these are the costs and the checks.",
1487
+ "",
1488
+ "- **Client bundle size / tree-shaking.** `@xano/sdk` is `sideEffects: false` and pulls",
1489
+ " no Node built-ins, so a bundler drops unused SDK exports. But importing a **def** for its",
1490
+ " `getPath()`/`verb`/`getUrl()`/`getChannel()` also pulls whatever its `stack` references:",
1491
+ " the `s.*`/`c.*` factory CALLS run at module load to BUILD it. Types are free.",
1492
+ " \u26A0 A FLOOR \u2014 **~267 kB minified (~65 kB gzipped)** for the FIRST def; splitting modules",
1493
+ " never removes it. The floor is the RUNTIME, not the def: a second or much richer def",
1494
+ " adds ~2 kB, so trimming a def does not shrink it.",
1495
+ " Fix: `xanosdk routes <entry> --emit xano/routes.gen.ts` (`paths` is an accepted alias) \u2014 verbs, paths, and sockets as",
1496
+ ' plain data importing NOTHING, still compile-checked: `routePath("GET blog/{slug}", { slug })`',
1497
+ ' `channelPath("rooms/{room_id}", { room_id })`, `socketUrl("chat", baseUrl)` (tenant base',
1498
+ " URLs lifted to `wss://h/ws/<tenant>:<canonical>`). A rename is a type error, not a 404.",
1499
+ "- **Verifying a def outside a bundler.** Inside a bundler (Vite/webpack) importing a",
1500
+ " query def to read `getPath()`/`verb` works directly. To spot-check from Node, run a REAL",
1501
+ ' file with `tsx <file.ts>` **from inside the project root** \u2014 not `tsx -e "import \u2026"`',
1502
+ " (its CJS-preparse mis-resolves the package `exports` map \u2192 ERR_PACKAGE_PATH_NOT_EXPORTED),",
1503
+ " and not bare `node file.ts` (chokes on the `.js`-specifier intra-workspace imports the",
1504
+ " xanosdk CLI's own loader resolves). Running from outside the project root also breaks",
1505
+ " the `@xano/sdk` specifier resolution.",
1506
+ "",
1507
+ "**Reading the injected backend URL.** `xanosdk deploy <entry> --static <dir>` writes the deployed env's URL",
1508
+ "into every html document: a prerendered build serves a different one per",
1509
+ "route, and a route without the global renders fine while every call goes to the wrong",
1510
+ "origin. Read it at runtime with a build-time fallback:",
1511
+ " const HOST = (typeof window !== 'undefined' && window.XANO_HOST) || import.meta.env.VITE_XANO_HOST;",
1512
+ "The scaffold's `lib/api.ts` types it (`interface Window { XANO_HOST?: string }` \u2014 `undefined` in dev)",
1513
+ "and exports this as a `string`, `XANO_HOST`; outside a scaffold declare the global yourself.",
1514
+ "In LOCAL DEV there is no injected global, so the fallback is what answers: set",
1515
+ "`VITE_XANO_HOST` in a `.env.local` beside `.env.example` at the PROJECT ROOT. The",
1516
+ "scaffold's vite config sets `envDir` there (its `root` is `frontend/`, and Vite",
1517
+ "resolves `.env` files against `root`) \u2014 without it the var reads as undefined, the",
1518
+ "host falls back to '', and every call 404s off the dev server.",
1519
+ '\u26A0 It is INJECTED in bracket form \u2014 `window["XANO_HOST"]="\u2026"` \u2014 so verifying a deploy',
1520
+ "by grepping `window.XANO_HOST` matches nothing and reads as a failed inject. Grep the",
1521
+ "bare `XANO_HOST` token.",
1522
+ "**Frontend without the backend.** `xanosdk publish <dir> [--to <backend>]` uploads an",
1523
+ "already-built directory and nothing else \u2014 no compile, no import (scaffold script",
1524
+ "`npm run xano:deploy:frontend`). `publish` does not reach a local engine: after a local-engine",
1525
+ "deploy, pass `--to ephemeral`, or give the engine the frontend with",
1526
+ "`xanosdk deploy <entry> --local-engine --static <dir>` (served at `http://<prefix>.localhost:<port>`).",
1527
+ "**Server-rendered SvelteKit.** `adapter()` from `@xano/sdk/sveltekit` (needs `esbuild`) writes",
1528
+ "the static half at the build's root plus a server half in `.xano-ssr/`. A local engine renders every",
1529
+ "path that is not a file through it (`load` reads the engine's own URL as `XANO_HOST` from",
1530
+ "`$env/dynamic/private`; a rendered page gets the deploy's `window.XANO_HOST` too). Any other host",
1531
+ "is not sent `.xano-ssr/`: it serves the static half and answers other paths with the `404.html`",
1532
+ "shell, where a route with a server `load` fails (no `__data.json`). A scaffolded project configures",
1533
+ "SvelteKit in `vite.config.ts`'s `sveltekit({ adapter })`, where `svelte.config.js` is ignored.",
1534
+ "\u26A0 `.xano-ssr/server.js` inlines `$env/static/private` values; it is never served (`/.xano-ssr/` answers 404).",
1535
+ "The injected `XANO_HOST` is the DESTINATION's URL, so one build published to two",
1536
+ "destinations serves two different documents. `--release <name>` checks only that the",
1537
+ "release exists; `--branch <label>` (workspace only) refuses unless that label is LIVE \u2014",
1538
+ "a label match, not a content comparison. `--json` reports `published` and `verified` separately.",
1539
+ "**Displaying a stored file.** A file column comes back as `{ path, name, type, size,",
1540
+ "meta, access, url }`. \u26A0 Do NOT use its `url`: on a tenant-scoped environment that field",
1541
+ "addresses the instance host WITHOUT the `/tenant/<name>` segment and 404s, silently \u2014",
1542
+ "as a broken `<img>`, with every API assertion still passing. Build the URL from `path`",
1543
+ "instead: `fileUrl(row.avatar, HOST)` returns `null` for",
1544
+ "an absent file and is correct on an ephemeral and an instance workspace alike."
1545
+ ];
1546
+ const errors = [
1547
+ "### Request-time (deployed) failures",
1548
+ "",
1549
+ '- `Unable to locate input: <name>` \u2014 `inp()` names an undeclared input, or an attached middleware reads its own `input` (never bound). Declare it; a stack value is `ref("var.field")`; middleware reads the body with `s.util.get_all_input`. `llms.txt`; `llms/kinds-core.md`.',
1550
+ "- `Unable to locate var: <a.b>` (HTTP 500) \u2014 a dotted `ref` into a null base (a `db.get` miss), or a column absent from the row (`f.password` is `internal`). Guard existence first, or use the null-safe `safe` option; name the column in `output`. In a `post` middleware, `<var>.vars` from `s.util.get_all_input` holds `{ status, result }` (plus `payload` on an error), not the request. `llms.txt`; `llms/kinds-core.md`.",
1551
+ "- `Missing var entry: <name>` inside `s.expect.to_throw` \u2014 the body runs in an isolated var stack; bind what it needs inside the body. `llms/tests.md`.",
1552
+ "- `Missing param: field_value` (HTTP 400) \u2014 a `db.*` match argument resolved to `null` (a safe ref, a nullable foreign key). Guard existence before the lookup; an optional FK stores `0` and is read with field-match `db.get`. `llms.txt`; `llms/fields.md`.",
1553
+ "- `Value is less than the minimum value of 1` (HTTP 400) \u2014 a `db.get` given `0` in an editor-authored workspace (SDK field-match `db.get` answers 200 + null). `llms/statements-data.md`.",
1554
+ "- `Invalid op: <op>` \u2014 a `cmp` operator (`in`, `like`, \u2026) in a RUNTIME condition, which takes the `expr` set only; write `or(expr(...), expr(...))`. `llms/kinds-core.md`.",
1555
+ "- `Unsupported param format` / `Unsupported parameter reference` \u2014 an aggregate or `eval` `name` that is not a bare column, or a bare `eval` name for a joined column. `llms/statements-data.md`.",
1556
+ "- `ParseError: Invalid value for param` (HTTP 400) \u2014 `contains`/`@>`/`overlaps` on a text column (use `includes`), or a joined column qualified without `tableAlias`. `llms/statements-data.md`.",
1557
+ "- `ERROR_FATAL: <Type> does not exist: <type>:<n>` \u2014 `s.api.call`/`s.task.call`/`s.trigger.call`/`s.workflow_test.call` outside a workflow test; share logic via `defineFunction` + `s.function.run`. `llms.txt`.",
1558
+ '- `ERROR_FATAL "Unable to decode."` \u2014 a populated JSON string where the engine expects `c.obj`\'s form; write `c.obj({...})` or `obj({...})`. `llms/values.md`.',
1559
+ '- `Precondition failed.` in place of your message \u2014 `error` was a bare string; pass `c.text("\u2026")`. `llms/fields.md`.',
1560
+ "- `Param: token - Text filter requires an integer, float, string or boolean value` \u2014 `s.api.call` `auth.token` given a tagged `Value`; it must be a bare string. `llms/statements-calls.md`.",
1561
+ "- Every `s.db.add` into one table 400s naming a column while complaining about its VALUE \u2014 the column is named `run`. `llms.txt`.",
1562
+ '- HTTP 200 with an EMPTY body where `0` was expected \u2014 a bare `returnType: "count"` of zero; wrap it as `{ count: ref("n") }`. `llms/statements-data.md`.',
1563
+ "- HTTP 200 with `[]` from a `db.query` that matches rows \u2014 a paged query (`metadata: true`, the default) whose `output` names bare columns; the selection applies to the ENVELOPE, so prefix them (`items.<column>`) or set `metadata: false` inside `paging`. `llms.txt`; `llms/statements-data.md`.",
1564
+ "- Every field mapped off an `s.api.request` result reads `null` \u2014 the upstream answered an error (a 403 for a missing `User-Agent` is the common one) and its body arrived as ordinary `response.result`; send a `User-Agent` and gate on `response.status`. `llms/statements-calls.md`.",
1565
+ '- A lambda returns error TEXT with HTTP 200 (`Could not resolve "node:crypto"`, an undefined-binding message) \u2014 the body threw; use the preloaded globals, the surface\'s bindings, and `capture`. `llms/lambda.md`.',
1566
+ "- `Unable to locate request.` (404) on an endpoint that deployed \u2014 a lowercase verb, a `.` in the name, or a CORS `mode` typo that dropped the whole API group. `llms/kinds-core.md`.",
1567
+ '- A browser call fails on a missing `access-control-allow-origin` while deploy looks fine \u2014 CORS `mode: "custom"` with `"*"` or an unmatched origin. `llms/kinds-core.md`.',
1568
+ "- A websocket is closed right after the handshake \u2014 a refused `connect` gate: an empty/falsy return, a crash, or a gating trigger with no `response`. `llms/triggers.md`.",
1569
+ "- `to_throw failed - response is ok` around an auth-refused call \u2014 `ERROR_CODE_ACCESS_DENIED` carries no message; the auth gate is not reachable from a workflow test. `llms/tests.md`.",
1570
+ "- Output reads `Hi [object Object]` \u2014 a tagged value inside a JS template literal; compose at runtime with `withFilters` + `fl.concat`. `llms.txt`.",
1571
+ "",
1572
+ "### Build-time and tooling failures",
1573
+ "",
1574
+ '- `export --strict` fails on a warning whose shape is meant (a fixture pinning a hazard) \u2014 accept it on ITS def only with `diagnostics: { allow: ["<code>"] }`, which takes any warning code that def raised (never an error) on every def kind. Accepted warnings are still listed: `export({ accepted: [] })`, `--json` `accepted[]`. An allowed code the def no longer raises warns `diagnostics.allow-unused`.',
1575
+ '- `must be ES modules` \u2014 package.json says `"type": "commonjs"`; set `"type": "module"`. `llms.txt` Quickstart.',
1576
+ "- A response types as `StackTupleWidened` \u2014 a `Statement[]` helper was spread into the stack; return `statements(...)`. `llms.txt`; `llms/statements-runtime.md`.",
1577
+ '- `ERR_PACKAGE_PATH_NOT_EXPORTED` \u2014 `tsx -e "import \u2026"`, or running from outside the project root; run a real file with `tsx <file.ts>` from the root. `llms/client.md`.',
1578
+ "- `Missing statement: mvp:placeholder` on import \u2014 an unconfigured slot in a pulled tree; replace it with the statement it stands in for. `llms/legacy.md`.",
1579
+ '- A CLI command fails under `--json` or piped (unless stdout carries data): stdout is `{ ok: false, error: { code, message, exitCode, details?, suggestion? } }`, as does a failed release write. Codes: `SDK_SEED_IN_STATIC` (`details.leaks[]` \u2192 `publicSeed`), `SDK_EXPORT_INVALID` (`details.diagnostics[]`, `--strict` too), `SDK_DRIFT`, `SDK_BRANCH_TAKEN`, `SDK_BRANCH_LIVE`, `SDK_RELEASE_NAME_TAKEN`, `SDK_RELEASE_FILES_NOT_CARRIED`, `SDK_LIVE_BRANCH_MISMATCH` (`details.conflictsWith`), `SDK_IDENTITY_CONFLICT`, `SDK_KIND_CONFLICT`, `SDK_PRUNE_OUT_OF_SCOPE`, `SDK_IMPORT_REFUSED`, `SDK_SHARED_SCHEMA_CHANGE`, `SDK_CONSTRAINT_VIOLATION`, `SDK_CREDENTIAL_REJECTED` (`details`: `{ profile, credentialType, instance, workspaceId, signIn }`; ids null on a refused env refresh), `SDK_USAGE` (`details.reason: "needs-confirmation"`: run its `Re-run as`), `SDK_ERROR`. Landing refusal `details.refused`: identityConflict, renamePending, kindConflict, uniqueViolation, tableTrigger, importInProgress, importRefused, pruneOutOfScope. `warnings[]` / `details.warnings` / `accepted[]` entries: `{ code, message, subject? }` (`subject`: the def\'s `{ kind, name }`). Backend documents add `selector` and `workspaceId`.',
1580
+ "- A CLI command reports `could not reach <url>: fetch failed (<code>)` \u2014 blocked egress, not a bad credential; fix it before `xanosdk login`, which rotates a single-use refresh token.",
1581
+ "",
1582
+ "### CLI exit codes",
1583
+ "",
1584
+ "A failure document's `exitCode` matches.",
1585
+ "",
1586
+ "| Code | Meaning | Next step |",
1587
+ "|---|---|---|",
1588
+ "| 0 | Success, and a `delete` of a missing name (`alreadyGone`). | \u2014 |",
1589
+ "| 1 | A usage error (a missing local file included), a refused request, or anything not listed. | Read the message. |",
1590
+ "| 2 | Ran and disagreed: `preflight` failed, `workspace diff` differed, `promote`, a release or an import did not land as sent or hit a conflict, the `init --from`/`pull` round-trip check failed, a lock or branch-label conflict, or `init` could not install its dependencies or an add-on (scaffolded anyway). | Fix what it reports. |",
1591
+ "| 3 | `deploy --static`: the backend landed, the static site did not. | Deploy the site again. |",
1592
+ "| 4 | `deploy --require-microservices`: a microservice was not ready in time. The backend landed. | Check the microservice. |",
1593
+ "| 5 | `test` / `deploy --test`: the suite ran and a test failed. A deploy is not retracted. | Fix the test or the code. |",
1594
+ "| 6 | `test`, `deploy --test`: the suite could not be reached. | Check the backend; rerun. |",
1595
+ "| 7 | `upgrade --check`: an upgrade is available. | `xanosdk upgrade`. |",
1596
+ "| 8 | A named backend could not be addressed (or a named release, profile, branch, test, module, lock entry, `--seed` guid or local-engine version is missing): gone, expired, unreachable, a stopped local engine, or a busy import. | Retry, or redeploy. |",
1597
+ "| 9 | A write whose outcome is unknown, Ctrl-C during a write included. | Read the target's state before retrying. |",
1598
+ "| 130 | Ctrl-C outside a write or install. | \u2014 |"
1599
+ ];
1600
+ const kindsAgentMcp = [
1601
+ "- `mcpServer({ name, guid?, description?, instructions?, docs?, enabled?, canonical?, spec?, tags?, history?, tools?, prompts?, resources?, oauth?, llm?, output? })` \u2014 an MCP toolset. `llm?`/`output?` are the same blocks `agent()` takes and are usually absent: an MCP server and an agent are ONE stored object distinguished by `type`. Returns a handle with `getPath()`/`getUrl(baseUrl)` for the Streamable-HTTP endpoint \u2014 `getUrl` is NOT idempotent, a `baseUrl` already carrying an `/x2/mcp/<\u2026>/stream` path THROWS, so resolve ONCE from the instance base URL. Fires on an ephemeral. A TENANT rides AFTER the prefix (`/x2/mcp/tenant/<tenant>/<canonical>/<token>/stream`), not as a base-URL segment: `getUrl` LIFTS a `/tenant/<name>` out of the base and re-places it there, so `getUrl(window.XANO_HOST)` is correct as-is; concatenating them by hand builds an unregistered route that 404s. Pass `{ tenant }` when the base URL cannot name it (a tenant on its own domain).",
1602
+ ' - `tools?`: a `ToolsetToolEntry[]`. Pass the `tool()` HANDLES directly (`tools: [saveNote]`); use the `{ tool, enabled?, auth?, type?, resourceUri?, toolMeta? }` wrapper when an entry needs more. `type: "resource"` (+ a required `resourceUri`) is the LEGACY way to publish a tool as an MCP resource \u2014 author a `resource()` and list it in `resources` instead; `toolMeta` is extra metadata on a tool entry. The two payload fields are per-type and supplying the other one is refused, because the engine stores it against neither. `auth` names an auth **table** (a `table({ auth: true })` handle or its name) \u2014 per-tool, and the only gate there is. \u26A0 It is also what gives the tool a CALLER, on an `agent()` exactly as on an MCP server. `auth()` inside a tool whose entry names no table does NOT return null: the call answers a normal result carrying `{"code":"ERROR_FATAL","message":""}`, so the model reads a successful tool result and reports success while nothing is written. A caller token does not rescue it \u2014 the ENTRY supplies identity. Spell it `{ tool: saveNote, auth: users }`; `export()` warns. An entry naming no tool THROWS at export; a deliberate raw `id: 0` warns and carries through.',
1603
+ ' - `prompts?` / `resources?`: `prompt()` / `resource()` HANDLES (or names), or a `{ prompt, enabled?, auth? }` / `{ resource, enabled?, auth? }` wrapper. `auth` names the auth table and is what gives the stack a caller: a prompt/resource reading `auth()` behind an entry with none answers every `prompts/get` / `resources/read` with `-32603 "Access Denied"` (`export()` warns `toolset.primitive-reads-auth-ungated`). Listing one target twice THROWS; two resources with one `uri` on one server fail `export()` (`mcp.resource-uri-duplicate`). MCP-server only \u2014 `agent()` takes tools only.',
1604
+ " - `history?` is the server tier for its tools, prompts AND resources alike; an object's own `history` wins. The workspace tier (`workspaceConfig({ history })`) has `prompt` and `resource` keys, both default on.",
1605
+ ' - `oauth?`: per-server sign-in; omit and nothing changes. Union on `mode`; `authTable` (REQUIRED, auth `table()` handle or name) is whose row the signed-in user is \u2014 `auth()` on the whole server, `t.auth` in its trigger. Hosted (the platform is the authorization server): `{ mode: "hosted", authTable, loginUrl }` \u2014 clients are sent to `loginUrl` (YOUR page, which is also the consent screen) with `mcp_request`; show `s.mcp.oauth.request`, sign the user in, then your endpoint calls `s.mcp.oauth.complete` with their `decision` and the page redirects to the URL it binds. External (your IdP issues tokens): `{ mode: "external", authTable, issuer, column, audience?, audienceAck?, claim?, claimAck?, preset?, allowedClientIds? }` \u2014 token `claim` (default `sub`) matched against `column`; `preset` is `auth0|clerk|workos|entra|okta|cognito|generic`. `loginUrl`/`issuer`/`audience` take `env("NAME")` (resolved per environment).',
1606
+ ' - `export()` FAILS: hosted without `loginUrl` (`mcp.oauth-hosted-incomplete`); external without `issuer`/`column` (`mcp.oauth-external-incomplete`); `preset: "clerk"` without `allowedClientIds` (`mcp.oauth-preset-client-ids`); `claim` other than `sub`/`oid`/`email` without `claimAck` (`mcp.oauth-claim-unacked` \u2014 users can edit it at most providers). Unknown `mode`/`preset` THROWS; so do the dropped `consent`/`branding`/`trustedClients`.',
1607
+ '- `prompt({ name, guid?, description?, docs?, title?, icons?, tags?, history?, input?, stack?, response?, responseShape?, middleware? })` \u2014 an MCP prompt; takes `registerPrompts`. `input` is the prompt\'s arguments (each field\'s `description`/`required`/enum `values` reach the client; enum values also answer completion requests). `response`: a text value is ONE user message; a list of `{ role: "user" | "assistant", content }` is sent in order: `s.set_var("m", obj({ list: [obj({ role: c.text("user"), content: ref("body") })] }))`, `response: ref("m.list")`. Without `response` or a top-level `s.return`, `prompts/get` fails.',
1608
+ "- `resource({ name, uri, guid?, description?, docs?, mimeType?, title?, icons?, annotations?, tags?, history?, input?, stack?, response?, responseShape?, middleware? })` \u2014 an MCP resource; takes `registerResources`. Clients address it by `uri`, never `name`.",
1609
+ ' - `uri`: a literal (`"docs://readme"`, no `input`) or an RFC 6570 LEVEL-1 template (`"orders://{region}/{order_id}"`). It must start with a scheme; each `{variable}` is letters/digits/`_`, one per brace pair, never two adjacent. `input` must be EXACTLY the template\'s variables, each a single `text`/`int`/`decimal`/`bool`/`enum`/`email`/`uuid` \u2014 anything else THROWS. A value that does not convert is refused before the stack runs.',
1610
+ " - `response`: a string is returned as text; non-UTF-8 content (a file, binary) as a base64 blob. `mimeType?` omitted \u2192 derived from what the stack returns.",
1611
+ ' - `annotations?`: `{ audience?: ("user" | "assistant")[], priority?: 0..1, lastModified?: ISO 8601 string }`.',
1612
+ '- MCP metadata, on `tool`/`prompt`/`resource`: `title?` (display name clients show instead of `name`); `icons?: { src, mimeType?, sizes?, theme? }[]` \u2014 `src` is `https:` (\u2264 2048 chars), a base64 `data:` URI of PNG/JPEG/WebP (\u2264 64 KiB), or `hostedFile("./icon.png", import.meta.url)`, a repo file served at each backend\'s own absolute URL (a pull keeps the `/vault/\u2026` path); SVG, `http:` and every other scheme THROW; `mimeType` is `image/png|jpeg|webp`, `sizes` a list (`["48x48"]`, `["any"]`), `theme` `light|dark`. Tool only: `annotations?: { readOnlyHint?, destructiveHint?, idempotentHint?, openWorldHint? }` \u2014 hints; an unset one is derived by the platform \u2014 and `output?`, a structured-output schema in the `input` grammar: a returned plain object then reaches the client as `structuredContent` plus a text copy, and a result that does not match is a tool error naming the field.',
1613
+ '- `s.mcp.elicit({ key, message, input?, as? })` \u2014 ask the MCP client\'s user mid-call; binds `{ action: "accept" | "decline" | "cancel", content? }` (`content` only on accept, keyed by the `input` field names). `key`: a stable name unique in the stack; `message`: string or value. `input` is the form: a FLAT set of `text`/`email`/`date`/`int`/`decimal`/`bool`/`enum` fields (an enum may be a list); nested objects, files, JSON, timestamps, `password` and `sensitive` fields THROW. Works in tool, prompt and resource stacks.',
1614
+ ' - \u26A0 The WHOLE stack re-runs from the top when the user answers \u2014 everything BEFORE an elicit runs once per round trip, so ASK FIRST, then act. A write before an elicit (`s.db.*` writes, raw SQL, a non-GET `s.api.request`, `s.util.send_email`, `s.function.run`/`.call`, `s.tool.call`) makes `export()` warn `mcp.write-before-elicit`; accept a deliberate one per def with `diagnostics: { allow: ["mcp.write-before-elicit"] }`. An elicit inside `s.db.transaction` commits the transaction\'s earlier writes on every round trip.',
1615
+ ' - Input is collected only from clients on MCP `2026-07-28` or later that declare form elicitation. Every other caller \u2014 an older client, a debug run, an agent, a workflow test \u2014 gets `{ action: "cancel" }` and the stack carries on, so ALWAYS handle `cancel`. A runtime error inside an async function run.',
1616
+ "- `s.mcp.progress({ progress, total?, message? })` \u2014 report progress to a client that asked for it (a no-op otherwise); values never go backwards (a lower `progress` is sent as the last one).",
1617
+ '- `s.mcp.oauth.complete({ request, decision, unsafe_user_id?, as? })` \u2014 run by the endpoint your `loginUrl` page calls after sign-in; `request` is the page\'s `mcp_request`. Signs in the CALLER (`auth()`, an `authTable` row whose session token is under 10 minutes old) and binds the single-use URL (text) to redirect to. `decision` (`"approve" | "deny"`, REQUIRED) is what the user chose on your page. \u26A0 `unsafe_user_id` names the row directly, skipping the caller checks \u2014 anyone reaching that endpoint signs in as that user.',
1618
+ "- `s.mcp.oauth.request({ request, as? })` \u2014 a pending sign-in's verified details for your `loginUrl` page to show before the user approves: `{ client_name, client_logo, callback_host, client_verified, server_name }`. Does not consume it.",
1619
+ "- `s.mcp.oauth.revoke({ mcp_server, user_id? | grant_id? | all?, as? })` \u2014 end hosted sign-ins on a server: EXACTLY ONE of `user_id`, `grant_id`, `all: c.bool(true)`, else `export()` fails (`mcp.oauth-revoke-target`). Binds `{ revoked }`.",
1620
+ "- `agent({ name, guid?, description?, docs?, enabled?, canonical?, tags?, history?, llm, tools?, output? })` \u2014 an LLM orchestrator. No top-level `instructions`/`prompt`/`spec` \u2014 the prompt lives under `llm`. Invoke from a stack with `s.ai.agent.run({ agent, args })`.",
1621
+ ' - `llm` (REQUIRED): typed provider settings, a discriminated union on `type` (`"xano-free" | "anthropic" | "openai" | "google-genai"`). Shared fields: `systemPrompt?`, `maxSteps?` (default `5`), and `prompt?` XOR `messages?` (genuinely exclusive: both is a type error and throws \u2014 the engine stores ONE `prompt_type`, so one would be dropped); plus provider fields (`apiKey?`, `model?`, `temperature?`, `reasoningEffort?`, \u2026). String fields accept Twig placeholders \u2014 `{{ $args.x }}` for run inputs (the `args` of `s.ai.agent.run`), `{{ $env.NAME }}` for env vars.',
1622
+ " - `tools?`: same `ToolsetToolEntry[]` as `mcpServer` \u2014 bare `tool()` handles, or a `{ tool, enabled?, auth? }` wrapper when one needs `enabled`/`auth`. A tool reading `auth()` MUST use the wrapper naming the auth table \u2014 a bare handle runs it with no caller (see `mcpServer`'s `tools?`).",
1623
+ ' - `output?`: `{ schema: Record<string, input.*>, enabled? }` \u2014 structured-output schema. `schema` is a named-field record authored with the `input.*` catalog, exactly like a `defineFunction`/`query` `input:` map (the stored `structuredOutputsSchema` is the same wire shape as function inputs). e.g. `output: { schema: { priority: input.enum(["low","high"]), summary: input.text() } }`. When you pass the agent *handle* to `s.ai.agent.run({ agent })`, `.result` is typed straight from this schema \u2014 no `resultShape` witness needed (the shape is declared once). `resultShape` remains only to override that, or to type an agent referenced by bare name.',
1624
+ ' - **Run + read recipe (`s.ai.agent.run`):** bind the run to a var (`s.ai.agent.run({ agent, args, as: "run" })`) \u2014 it produces a rich envelope, and the completion is at **`.result`**. Read one structured field with a dotted ref (`response: ref("run.result.priority")`) or the whole completion (`ref("run.result")`, typed from `output.schema`); persist it in a later step the same way (`s.set_var("summary", ref("run.result.summary"))`). `args` is a plain object of run inputs (`{ topic: inp("topic") }` \u2014 raw literals are fine, e.g. `{ max_steps: 3 }`) surfaced to the agent as `{{ $args.topic }}`. `.toolCalls` is the FINAL step\'s calls only (often `[]`); every call is a `.steps[].content[]` entry of `type: "tool-call"`.',
1625
+ " - **Marketplace:** for chat history, use `@xano-sdk/chatbot`; for vector RAG, use `@xano-sdk/vector` (`tools: [vector.searchTool]`). Find add-ons with `xanosdk marketplace search <q>`.",
1626
+ "- Both kinds persist under the `toolset` payload key, so a same-name `agent` + `mcpServer` pair derives ONE guid and `export()` throws \u2014 give them distinct names."
1627
+ ];
1628
+ const kindsRealtime = [
1629
+ "- **Realtime** \u2014 the only three-level chain: `realtimeServer` owns `realtimeChannel`s, which own `realtimeMessage` handlers. Pass the HANDLE, not a name (a channel path is unique only within its server).",
1630
+ " - `realtimeServer({ name, guid?, description?, enabled?, canonical?, tags?, history? , middleware? })` \u2014 the container.",
1631
+ " - `enabled` defaults to **false** \u2014 the one `enabled` in the SDK that does.",
1632
+ " - An enabled server with no active channel still refuses the handshake.",
1633
+ " - `realtimeChannel({ name, server, guid?, description?, active?, input?, anonymousClients?, presence?, publish?, conversation?, delivery?, rateLimit?, tags?, history?, middleware? })`",
1634
+ ' - `name` is a PATH (`"lobby"`, `"rooms/{room_id}"`); `input` types its `{param}` segments, NOT the payload. Every `{param}` MUST have a matching input, and that input must be a SCALAR and not a list (`json`/`object`/`array: true` have no URL form), or `realtimeChannel()` THROWS. `required: true` is NOT checked and is not needed \u2014 segment counts must match, so the segment is always present at join. Name charset as query (`A-Za-z0-9_-/{}`, max 200), and so is `tool`; `realtimeMessage` is NARROWER \u2014 no `/` or `{}`.',
1635
+ " - Matching is STRICT: a literal segment beats a param (`rooms/lobby` and `rooms/{room_id}` coexist); segment counts must be EQUAL (`rooms/{room_id}` does NOT match `rooms/42/edit`); literals are CASE-SENSITIVE; an empty segment is REJECTED, not collapsed (a leading/trailing/doubled `/` matches nothing). `realtimeChannel()` refuses it and a partial-segment `{param}`; `getChannel()` a value outside `[A-Za-z0-9_-]`.",
1636
+ " - An INACTIVE channel reports the same error as a nonexistent one \u2014 deactivating leaks nothing.",
1637
+ " - `middleware?: { pre?, post? }` is the CONTAINER tier, on the channel and on the server alike \u2014 the default chain every message inherits, the same shape an api group has. Full chain: message -> channel -> server -> branch/workspace, mirroring `history`. Put a realtime auth or rate-limit guard here rather than repeating it on every `realtimeMessage`.",
1638
+ " - `anonymousClients` also gates CONNECT: a tokenless socket connects only if an active channel of its server sets it (no server flag); each channel admits its own join.",
1639
+ ' - `publish?: { who?: "nobody"|"anyone"|"authenticated", direct? }` \u2014 `who` defaults to `nobody`: nobody can publish until you set it. `direct` (default false) allows DIRECT messages \u2014 a frame whose `options.to = { dbo_id, row_id }` addresses one USER (`dbo_id` = the auth table\'s id as `s.realtime.get_session` reports it, `row_id` = the user\'s row id) and reaches every socket of theirs on the channel, no one else; the sender gets a receipt `{ delivered_local, direct: true }`. `direct` REPLACES `who` for such a frame rather than adding to it (`direct: true` with `who: "nobody"` delivers direct messages and refuses broadcasts); an anonymous sender is refused. A direct message is never stored in the transcript or the `at_least_once` stream, so an offline user never gets it. \u26A0 An `options.to` naming no usable identity is sent as an ordinary broadcast to the whole channel where `who` admits the sender.',
1640
+ " - `conversation?: { enabled?, limit?, ttl? }` \u2014 the client-visible TRANSCRIPT replayed to a joiner (distinct from `history`, which is execution history). \u26A0 `limit` DEFAULTS TO 0 AND 0 MEANS OFF: `{ enabled: true }` alone records nothing and replays nothing, silently. `ttl` is an IDLE expiry of the WHOLE transcript, refreshed by every write (an active channel never ages out; a silent one loses all of it at once) \u2014 NOT a per-message age cap.",
1641
+ ' - `delivery?: { guarantee?: "at_most_once"|"at_least_once", perRecipient? }` \u2014 `perRecipient` is independent of the guarantee, is a NO-OP unless the channel declares a `deliver` trigger, and costs a stack PER RECIPIENT PER MESSAGE. Per-viewer redaction needs BOTH HALVES \u2014 this flag AND an active `deliver` trigger bound to the channel \u2014 and with either missing the payload is delivered UNCHANGED to everyone; `export()` warns on each half alone.',
1642
+ " - `rateLimit?: { messagesPerMinute? }` \u2014 0 = unlimited, checked BEFORE the handler runs. A COST guardrail, not a security control: an anonymous client is bucketed per CONNECTION (reconnecting resets it), and it fails OPEN when its store is down.",
1643
+ " - `realtimeMessage({ name, channel, server?, guid?, description?, active?, auth?, deliverTo?, input?, middleware?, stack?, response?, responseShape?, history?, disabled?, tags? })` \u2014 the invocable unit (the realtime analogue of a query).",
1644
+ " - `input` types the message PAYLOAD. `server` is required only when `channel` is a bare path.",
1645
+ ' - `deliverTo?`: `"channel"` (default) | `"sender"` | `"others"` | `"explicit"`. \u26A0 `"explicit"` still delivers to NOBODY \u2014 nothing selects recipients from inside a handler, and `s.realtime.publish` (which originates an event INTO a channel) is not a substitute.',
1646
+ ' - Only `"channel"`/`"others"` fan out AND are RECORDED in the `conversation` transcript \u2014 a `"sender"` response is invisible to every future joiner.',
1647
+ ' - **Both input surfaces read as ordinary inputs:** `inp("body")` for a payload field, `inp("room_id")` for the channel\'s `{room_id}`. No session lookup, no frame parsing.',
1648
+ " - A path param is bound ONCE at join and read from the connection thereafter, never from the frame \u2014 a sender cannot claim a room it did not join.",
1649
+ " - `s.realtime.get_session({ as })` \u2014 the CALLER's realtime session for the current frame. FLAT shape:",
1650
+ ' - `authenticated` bool \xB7 `client_id` text (the AUTHED ROW ID as text, `""` anonymous) \xB7 `dbo_id` int (the auth TABLE\'s id \u2014 NOT the user\'s row id; `0` anonymous \u2014 to look the caller up use `client_id`. `dbo_id` is an int in the same position and typechecks, so a gate that keys on it finds no user and refuses EVERYONE) \xB7 `socket_id` int (transport id) \xB7 `channel` text (resolved path, `""` in a server trigger) \xB7 `params` object (bound path params, `{}` when none \u2014 `ref("session.params.room_id")`) \xB7 `extras` object \xB7 `opened_at` decimal.',
1651
+ " - Works in a realtime MESSAGE stack and in CHANNEL and SERVER trigger stacks; off that path it degrades to an anonymous session.",
1652
+ ' - For a path param prefer `inp("room_id")` in a MESSAGE; a lifecycle TRIGGER has only the session. Reach for the session when you need the CONNECTION (identity/extras) \u2014 "who is this sender" on an anonymous-client channel.',
1653
+ " - \u26A0 THREE UNRELATED THINGS ARE CALLED A CLIENT ID: `session.client_id` (app-facing identity), `session.socket_id` (transport), and a frame's `options.client_id` (the at_least_once CURSOR handle). Conflating the first and last breaks at_least_once for anonymous clients.",
1654
+ " - `s.realtime.publish({ server, channel, data, message?, authTable?, authId? })` \u2014 the PUSH direction: originate a server-authored event onto a channel from ANY stack, no client frame first.",
1655
+ " - `server` is the handle or its NAME (resolved by name, not guid); `channel` is the FILLED-IN path (`channel.getChannel({ room_id: 42 })`), never the template \u2014 a constant still carrying `{param}` THROWS at author time, and a constant `server`/`channel` naming nothing this workspace registers WARNS at export.",
1656
+ ' - A PER-ROW path whose id is only known at runtime is built as a value, not with `getChannel()` (which needs the id at author time): `withFilters(c.text("rooms/"), fl.concat(ref("room.id")))`, or `s.set_var` + `s.text.prepend`. A computed `channel`/`server` \u2014 a `ref`/`inp`, or a constant carrying a filter chain \u2014 is left alone by the export check.',
1657
+ " - DELIVERY-ONLY \u2014 fanned out as-is; does NOT invoke a `realtimeMessage()` handler even when `message` names one (a channel `deliver` trigger still runs).",
1658
+ " - SERVER-AUTHORITATIVE \u2014 bypasses `publish.who`, which governs CLIENTS. Authorize in your own stack.",
1659
+ " - \u26A0 FAIL-SOFT \u2014 a missing/disabled server or dead bus is swallowed engine-side, so a mis-targeted publish is SILENT with no result to check.",
1660
+ " - `authTable`/`authId` are ASSERTED attribution on the frame \u2014 not a credential, nothing validates them.",
1661
+ " - **Client recipe (derive, never hardcode):**",
1662
+ ' - `server.getUrl(baseUrl)` \u2192 `wss://<host>/ws/<canonical>` \u2014 accepts the `https://\u2026` instance base URL and normalizes the scheme. `channel.getChannel({ room_id: 42 })` \u2192 `"rooms/42"`, the path that goes in a frame\'s `channel` field. Both throw rather than guess. A canonical is minted by `xanosdk export <entry> --lock`.',
1663
+ " - Auth is a bearer token passed as the websocket SUBPROTOCOL: `new WebSocket(url, token)`. No token = an anonymous client, admitted only where `anonymousClients: true`.",
1664
+ ' - Frames are JSON `{ action: "join"|"leave"|"broadcast"|"ack"|"ping"|"presence", channel, type?, payload?, options?, id? }`. `broadcast` REQUIRES `type`: a `realtimeMessage` of that channel, else refused `Unknown message type`. You must `join` before you may `broadcast`, and the server\'s context is ready only a moment after `open` \u2014 an immediate first frame is refused.',
1665
+ " - `options` is `{ to?, client_id?, channel? }` \u2014 `to: { dbo_id, row_id }` makes the frame a direct message (needs `publish.direct`), `client_id` is the at-least-once cursor handle, and `options.channel` WINS over a top-level `channel`.",
1666
+ ' - \u26A0 KEEP THE SOCKET ALIVE: an idle connection is REAPED after ~10 minutes. A LISTEN-ONLY client (a feed or dashboard that joins and rarely publishes) MUST send `{ action: "ping" }` (answered `pong`) or any frame periodically or it silently drops.',
1667
+ " - Server frames: `join` (ack `{ joined: true, params }`, + `cursor`/`resumed` on at_least_once) \xB7 `message` \xB7 `replay` \xB7 `broadcast` \xB7 `presence_full`|`presence_join`|`presence_leave` \xB7 `conversation_start`|`conversation_end` (replayed frames flagged `conversation: true`) \xB7 `pong` \xB7 `ack` \xB7 `error`.",
1668
+ " - \u26A0 `broadcast` is a RECEIPT to the sender, not a delivery confirmation: `payload.delivered_local` counts recipients on the ANSWERING NODE ONLY, not the channel. It also carries `id` on at_least_once and `dropped: true` when the handler returned null.",
1669
+ " - `error` carries `payload.message`, plus `code`/`limit`/`retry_after` when rate limited. `rate_limited` is the ONLY code \u2014 do NOT switch on `code`.",
1670
+ " - An `error` is a per-frame refusal, NOT a disconnect \u2014 EXCEPT a failed handshake and a REFUSED `connect` trigger, which each send one and then CLOSE with code 4401.",
1671
+ " - **Tenant instances (isolated DB):** a tenant's realtime objects live in the TENANT's database, so BOTH halves of a client must name the tenant.",
1672
+ " - Socket: `server.getUrl(base, { tenant })` \u2192 `/ws/<tenant>:<canonical>`. \u26A0 A bare canonical on a tenant host resolves against the INSTANCE workspace instead.",
1673
+ " - That colon form is PECULIAR TO THE SOCKET. Every other tenant URL gives the tenant its OWN segment \u2014 the HTTP half of the same client is `https://<host>/tenant/<tenant>/api:<canonical>/\u2026`. NO request header is required for either.",
1674
+ " - Because the shapes differ, `getUrl` TRANSLATES a tenant base URL instead of concatenating: pass the `https://<host>/tenant/<name>` that `xanosdk status` prints (and that deploy injects as `window.XANO_HOST`) and the tenant is LIFTED into the socket form. So `getUrl(window.XANO_HOST)` needs no `{ tenant }`, and a CONFLICTING `{ tenant }` alongside it throws.",
1675
+ " - \u26A0 `getUrl`/`socketUrl` are NOT idempotent \u2014 a `baseUrl` that already carries a `/ws/<\u2026>` path THROWS. Resolve ONCE from the http(s) base; never feed a result back in.",
1676
+ " - Still pass `{ tenant }` explicitly for a tenant on its OWN DOMAIN \u2014 the hostname carries it for HTTP, but there is nothing in the URL for the socket to lift.",
1677
+ " - \u26A0 Tokens are tenant-scoped (audience `<tenant>:<license>`, not the bare license), so one minted through the instance workspace is REJECTED by a tenant's realtime server \u2014 authenticate and dial through the same tenant.",
1678
+ " - **Presence frames** (a `presence: true` channel only):",
1679
+ " - `presence_full` carries `payload.members`, an ARRAY: at join it EXCLUDES the receiver (first joiner: `[]`), on a re-request it includes it. `presence_join`/`presence_leave` carry a single `payload.member`.",
1680
+ " - A member is `{ id, dbo_id, authenticated, extras, joined_at }`: `id` the auth row id as a string (`\"0\"` anonymous), `dbo_id` the auth table's id (`0` anonymous), `extras` the connection's extras (`[]` if none), `joined_at` epoch SECONDS.",
1681
+ ' - Render `presence_full`, then apply deltas. One member per authenticated identity (a second tab fires no `presence_join`); anonymous ones all have `id` `"0"`, never collapsed.',
1682
+ " - Join order: `join` ack \u2192 `presence_full` \u2192 (others get `presence_join`) \u2192 conversation replay \u2192 `replay` frames.",
1683
+ ' - A joined client re-requests it with `{ action: "presence", channel }`, answered to the SENDER only; a socket that never joined is REFUSED.',
1684
+ " - **Conversation frames \u2014 the transcript hydrates the client, so DO NOT build a hydration endpoint.**",
1685
+ ' - On a `conversation` channel the replay is PUSHED automatically at join, unasked: `conversation_start` (`payload.count`) \u2192 the last `limit` messages, each a normal `action: "message"` frame carrying its ORIGINAL `type` and `payload` plus `conversation: true` and the original `ts` \u2192 `conversation_end`.',
1686
+ " - Render `message` frames identically either way: no fetch or table read is needed to paint the initial view.",
1687
+ " - \u26A0 `{ enabled: true }` alone is a no-op: `limit` defaults to 0 and 0 means RETAIN NONE (see `conversation` above) \u2014 always pass `limit`.",
1688
+ " - The POST-HANDLER broadcast payload IS the stored transcript row \u2014 a handler must broadcast everything the UI needs to render a past message (author name, id, `created_at`). Nothing else is replayed.",
1689
+ " - The transcript is a capped ring (`limit`, `ttl`), not storage: persist to a table only for durability, search, or reads beyond it.",
1690
+ ' - **`delivery.guarantee: "at_least_once"` is a CLIENT CONTRACT, not just a channel setting.**',
1691
+ ' - The client must ACK what it receives \u2014 `{ action: "ack", channel, id }`, confirmed by `{ action: "ack", channel, payload: { cursor } }`.',
1692
+ " - \u26A0 An ANONYMOUS client must ALSO send a durable `options.client_id` in its JOIN frame (once; later acks need not repeat it). WITHOUT one it has no cursor, its acks are SILENTLY IGNORED, and it degrades to at_most_once. An AUTHENTICATED client is keyed by identity and needs no `client_id`.",
1693
+ ' - The missed gap arrives after join as `replay` frames, oldest-first, each with an `id` to ack. \u26A0 A first join gets `cursor: "0"`: the WHOLE retained stream replays, repeating a `conversation` transcript.',
1694
+ ' - DISTINCT from the conversation transcript: `conversation_*` is the SHARED "what was said before I arrived", `replay` is the PER-CLIENT "what I missed while disconnected". Both may be on.',
1695
+ " - How far back `replay` reaches is sized by `conversation.ttl` (here a REAL per-message age cut, and it BEATS `limit`), else `conversation.limit`, else 1000 \u2014 even on a channel with no transcript enabled.",
1696
+ " - **What a message handler RETURNS decides delivery, and the failure directions are NOT symmetric.**",
1697
+ " - A returned value fans out per `deliverTo` and becomes the transcript row.",
1698
+ " - Returning NULL delivers NOTHING \u2014 the supported way to veto a message (the sender is told `dropped: true`).",
1699
+ " - A payload REJECTED by the declared `input` also delivers nothing; the detail goes ONLY to the sender.",
1700
+ " - \u26A0 But a handler that CRASHES FAILS OPEN: the sender's ORIGINAL, UNVALIDATED payload is broadcast to the channel unchanged. A handler doing redaction or authorization must NOT be the only thing between client input and subscribers.",
1701
+ ""
1702
+ ];
1703
+ const triggers = [
1704
+ "### Triggers",
1705
+ "",
1706
+ "**A trigger's `stack` is a callback \u2014 `stack: (t) => [...]` \u2014 to read a trigger",
1707
+ "input** (a plain list works when it reads none). That's the one",
1708
+ "shape that doesn't carry over from the other kinds: a trigger has no",
1709
+ "user-declared `input`, so its inputs are **implied by type** (fixed by Xano,",
1710
+ "not editable) and arrive through the typed **stack handle** `t` \u2014 you can't",
1711
+ "reference them without it. (`response` too \u2014 a callback, or a plain value that",
1712
+ "reads none.) `t` exposes exactly that trigger type's inputs; a wrong",
1713
+ "name is a compile error, not a runtime surprise. The seven trigger types are",
1714
+ "distinct root factories (not a namespace): `{tableTrigger, realtimeServerTrigger,",
1715
+ "realtimeChannelTrigger, mcpServerTrigger, agentTrigger, workspaceTrigger,",
1716
+ "errorTrigger}({ name, guid?, description?, active?, tags?, history?, ... })`.",
1717
+ "`history` is per trigger (omit to inherit the workspace's `history.trigger`, default",
1718
+ "off); a trigger's own failure reaches the error log only while it is on.",
1719
+ "",
1720
+ '- `tableTrigger({ name, table?, datasources?, actions?: {insert?,update?,delete?,truncate?}, stack })` \u2014 database/table trigger. `t.new` / `t.old` are the row **after** / **before** the change; `t.action` (`insert|update|delete|truncate`), `t.datasource`. Bind `table` to a `table()` handle and `t.new("col")` / `t.old("col")` are typed to that row (misspelled column = compile error). \u26A0 Insert\'s `old` and delete\'s `new` are `{}`, not null \u2014 branch on `t.action`, not a null check. Config-only (no response).',
1721
+ "- `realtimeServerTrigger({ name, realtimeServer, actions?: {connect?,disconnect?}, stack?, response?, responseShape? })` \u2014 realtime SERVER lifecycle (a client connecting to / disconnecting from the server, not a message). Inputs: `t.action` (`connect|disconnect`), `t.realtime_server`, `t.client`. Bind `realtimeServer` to a `realtimeServer()` handle (or its name).",
1722
+ " - `connect` GATES the connection \u2014 a denial sends an `error` and CLOSES the socket with code 4401 before it is ever ready, so it is a real front door, not an observer; same return shape as a channel `join` below (EMPTY/FALSY DENIES \u2014 INCLUDING a gating trigger with NO `response`, which returns nothing and so refuses every client).",
1723
+ " - A CRASH DENIES too \u2014 a gate that cannot answer must not admit. Both failure modes lock the door, so plan for a self-inflicted LOCKOUT (an unguarded drill into a null `db.get` raises \u2192 everyone refused), not a breach.",
1724
+ " - Gating is OPT-IN: a server with no `connect` trigger accepts every connection.",
1725
+ " - `disconnect` is OBSERVATIONAL (return ignored, throws swallowed \u2014 cleanup must always complete).",
1726
+ " - Both are SERVER-scoped, so `s.realtime.get_session` works but carries no channel path and no bound params.",
1727
+ "- `realtimeChannelTrigger({ name, channel, actions?: {join?,leave?,deliver?}, stack?, response?, responseShape? })` \u2014 realtime CHANNEL lifecycle. Inputs: `t.action` (`join|leave|deliver`), `t.channel`, `t.payload`, `t.client`. Bind `channel` to a `realtimeChannel()` handle \u2014 a bare path is NOT accepted (it is unique only within its server). The three actions have DIFFERENT postures, and the posture decides what the stack should return:",
1728
+ ' - `join` GATES the join (it runs before membership) \u2014 return `{ allowed: c.bool(true) }` (optional `reason` reaches the client) or any truthy value to admit, and an EMPTY OR FALSY RETURN DENIES, so a stack that just falls through \u2014 or a gating trigger with NO `response` \u2014 refuses everyone, and a CRASH DENIES too. ONCE the object carries an `allowed` key admission needs STRICTLY `true` \u2014 `1`/`"yes"` there DENIES. Compute it as `c.expression("\u2026")` or a `ref()` to a `set_var` boolean (`expr()` is not a `response` value). That is the inverse of a crashing message, which still delivers, and of `deliver` below.',
1729
+ ' - A lifecycle trigger\'s inputs are PINNED to those four, so a channel PATH PARAM is NOT among them \u2014 `inp("room_id")` RAISES, which crashes the gate and so REFUSES every client; take the param from `s.realtime.get_session` (`ref("session.params.room_id")`).',
1730
+ ' - A gate establishes NO auth: `auth("id")` reads 0 when authenticated, `ref("auth.id")` raises \u2014 identity is `t.client("permissions.row_id")` or the session. A SERVER connect/disconnect has no channel, so no params at all.',
1731
+ " - `leave` is OBSERVATIONAL (return ignored, throws swallowed).",
1732
+ " - `deliver` GATES delivery PER RECIPIENT \u2014 the per-viewer redaction tool and the most expensive action here (a stack per recipient per message), and it needs `delivery.perRecipient` on the channel to run at all \u2014 BOTH HALVES are required, so a `deliver` trigger on a channel without the flag NEVER RUNS and every subscriber receives the UNREDACTED payload (no error, no log line); `export()` warns on each half alone.",
1733
+ " - **`deliver`'s RETURN VALUES DO NOT READ LIKE A FILTER:** ONLY an explicit NULL drops the message for that recipient; an OBJECT replaces that recipient's payload; ANYTHING ELSE \u2014 INCLUDING `false`, `0`, `\"\"` \u2014 DELIVERS IT UNCHANGED, as does a crash. So `return false` from a yes/no redaction check SENDS the message it was written to suppress \u2014 return null instead.",
1734
+ ' - The delivered payload arrives NESTED, so read `t.payload("<field>")`, and `t.client` is the SENDER while `s.realtime.get_session` describes the RECIPIENT this run is for.',
1735
+ '- `mcpServerTrigger({ name, mcpServer, stack?, response?, responseShape? })` / `agentTrigger({ name, agent, stack?, response?, responseShape? })` \u2014 toolset connection. Bind with the `mcpServer()`/`agent()` def handle (or its name) \u2014 it resolves to the toolset guid at export. Raw numeric `objId` is the escape hatch, rarely right: ids are assigned at import, so a handle passed to `objId` is a type error, and binding nothing deploys a trigger that never fires. Inputs: `t.toolset` (`t.toolset("name")`), `t.tools`; an MCP server\'s also `t.prompts`/`t.resources`: return one filtered to narrow what the connection lists, omit it to list all \u2014 and `t.auth`, the signed-in user `{dbo, id, extras}` when the server has `oauth` (null otherwise). Response-bearing; the default returns every list input.',
1736
+ "- `workspaceTrigger({ name, actions?: {branch_live?,branch_merge?,branch_new?}, stack? })` \u2014 branch lifecycle. Inputs: `t.to_branch`, `t.from_branch`, `t.action`. Config-only.",
1737
+ '- `errorTrigger({ name, stack? })` \u2014 error-signature trigger. Inputs: `t.event` (`new|regression|fixed`), `t.id`, `t.signature`, `t.error` (`t.error("code")`/`t.error("message")`), `t.caller`, `t.statement`, `t.actor`, `t.count`, `t.first_seen`, `t.last_seen`, `t.fixed_at`. Config-only.',
1738
+ ""
1739
+ ];
1740
+ const filters = [];
1741
+ const fields = [];
1742
+ const values = [];
1743
+ values.push("## Values", "");
1744
+ for (const v of m.values.constructors) {
1745
+ if (v.legacy) continue;
1746
+ values.push(`- \`${v.name}${v.signature}\` \u2014 ${v.description}`);
1747
+ }
1748
+ values.push("", `Tags: ${m.values.tags.filter((t) => t !== "const:encoded").join(", ")}.`, "");
1749
+ fields.push("## Fields", "");
1750
+ fields.push(
1751
+ "Author table columns + function/API inputs with the typed catalog: `f.<type>(opts?)`",
1752
+ "for columns, `input.<type>(opts?)` for inputs. Common opts: `required`, `nullable`,",
1753
+ '`default`, `description`, `access` (a read omits `"internal"` columns, returns `"private"` ones; `InferRow`',
1754
+ 'is the full row, both kept), `sensitive` (masks the input\'s value in request logs; a read still RETURNS a sensitive column \u2014 `access: "internal"` keeps it out).',
1755
+ "**`nullable` defaults PER TYPE, matching the engine's own column-creation API**: `true`",
1756
+ "for `f.vector`, `f.uuid`, every `f.geo.*` and every file type (`f.image`/`f.video`/",
1757
+ "`f.audio`/`f.attachment`), `false` for everything else (text, int, decimal, bool, email,",
1758
+ "enum, json, object, password, date, tableRef); inputs add date, timestamp and file, so an",
1759
+ "omitted one binds `null` (a `required` one still accepts an explicit `null`). Pass `nullable` to override \u2014",
1760
+ "e.g. `f.geo.polygon({ nullable: false })`. An empty default becomes NULL only when",
1761
+ "nullable, so a non-null `f.vector(8)` column fails to create (`''` is not a vector).",
1762
+ "**`f.geo.*` values are `{ type, data }`, not GeoJSON.** The same shape goes in and comes",
1763
+ 'back. `type` per column: `point` \u2192 `"point"`, `data` `{ lng, lat }`; `multipoint` \u2192 `"points"`,',
1764
+ '`linestring` \u2192 `"path"`, `polygon` \u2192 `"poly"` (ring closed for you), `data` `[{ lng, lat }, \u2026]`;',
1765
+ '`multilinestring` \u2192 `"paths"`, `multipolygon` \u2192 `"polys"`, `data` `[[{ lng, lat }, \u2026], \u2026]`.',
1766
+ 'Any other spelling (`"multipoint"`, `"linestring"`, `"Point"`) 400s `Only strings are supported for:`.',
1767
+ 'Raw WKT text (`c.text("POINT(1 2)")`) is accepted on write too, but a read never returns one.',
1768
+ '`methods` (validators/transforms, names per type below): `"trim"`, `"min:8"`',
1769
+ '(a text arg keeps its `:`: `"startsWith:https://"`), or `{ name, arg }` \u2014 for unlisted names and `pattern`\'s error text.',
1770
+ "`f.json({children})` declares the nested shape stored INSIDE a json column \u2014 an ARRAY of",
1771
+ "`{name, type, methods?, children?}`, order-significant, distinct from the `FieldMap` that",
1772
+ "`f.object` takes positionally. Omit it for an unstructured json column.",
1773
+ "`f.enum(values)`/`f.vector(size)`/`f.object(children)`/`f.tableRef(table)` take a",
1774
+ "positional payload before opts \u2014 and still accept the standard `FieldOptions` after it",
1775
+ "(`f.enum([])`/`input.enum([])` are accepted: a pulled enum with no options, not one to",
1776
+ "author \u2014 it brands the column `never`, which `InferRow` surfaces as `undefined`.)",
1777
+ "(e.g. `f.tableRef(users, { required: true })` \u2014 only `min`/`max` are listed as tableRef",
1778
+ "methods below, but `required`/`nullable`/`description`/\u2026 apply like any field.)",
1779
+ "An **OPTIONAL foreign key wants a `0` sentinel, not `nullable: true`**. `f.tableRef` to an int-keyed table stores",
1780
+ "an `int`, and a null in it is unqueryable: `null` is never a legal `fieldValue`/`id`, so",
1781
+ "`s.db.get`/`edit`/`del` on that column answer HTTP 400 `Missing param: field_value` rather",
1782
+ "than matching nothing. Declare `f.tableRef(users, { required: true, default: 0 })` for",
1783
+ '"not set yet" \u2014 `s.db.get({ fieldName: "driver", fieldValue: c.int(0) })` matches no row and',
1784
+ "binds `null`, which is the answer the null was reaching for. `export()` warns on a literal",
1785
+ '`c.null()` in that slot. A uuid-keyed ref stays nullable even when `required` (a non-null one defaults to `""`, which no insert accepts).',
1786
+ "An `f.vector(size)` column is SEARCHED through `s.db.query`'s `eval` pipeline, not through",
1787
+ 'any `SearchOp`: give the table `index: [{ type: "vector", fields: [{ name: "embedding", op:',
1788
+ '"vector_cosine_ops" }] }]`, then rank with a distance filter + a sort on its alias (see',
1789
+ "`s.db.query` \u2192 `eval`). Without that pairing the column stores and indexes but nothing",
1790
+ "queries it.",
1791
+ "`{ array: true }` makes any `f.*` scalar a **list column** \u2014 `f.text({ array: true })`",
1792
+ "surfaces as `string[]` in `InferRow<typeof table>` (the column analogue of `input.list`).",
1793
+ "**Seed a table's starting rows with `table({ seed })`.** `seed` takes rows",
1794
+ "typed against the table's schema as a WRITE shape (a column without",
1795
+ "`required: true`, and the system columns, may be omitted; `null` needs",
1796
+ '`nullable: true`) \u2014 inline (`seed: [{ name: "\u2026" }]`), a FILE',
1797
+ '(`seed: seedFile("./seed.json", import.meta.url)`; path resolves against the DECLARING',
1798
+ 'file), or a thunk (`seed: () => import("./seed.json")`, async ok, `.default` unwrapped).',
1799
+ "Inline rows are TYPED against the schema at compile time; a `seedFile`/thunk seed is",
1800
+ "not \u2014 `xanosdk export`/`deploy` validates it, naming the row index and column.",
1801
+ "Timestamp seeds: epoch ms, `Date`, ISO 8601 date, or date-time with `Z`/offset; else THROWS.",
1802
+ "Int seeds: whole, in int64 (past 2^53: a string).",
1803
+ "\u26A0 Prefer `seedFile` for a file: a thunk's `import()` sits in",
1804
+ "YOUR module, so a bundler emits the JSON as a served chunk. Never put secrets",
1805
+ "in `seed`. `deploy --static` REFUSES a build carrying internal/sensitive seed values;",
1806
+ 'declare public ones (demo logins) on the table: `publicSeed: ["password"]`.',
1807
+ "A replacing deploy re-seeds cleanly; `--keep-data` writes no seed rows.",
1808
+ "`seed` is evaluated on EVERY build, so a computed value (`Date.now()`) is fine:",
1809
+ "the lock records no seed data.",
1810
+ 'A FILE column seeds from a repo file: `logo: hostedFile("./logo.png", import.meta.url)`;',
1811
+ "the row stores each backend's own copy. A wrong-kind file or a non-file column THROWS.",
1812
+ "A release carries no file bytes: `release create` refuses such rows and icons.",
1813
+ "Omit `id` and rows auto-number `1..N` (int PK) or take",
1814
+ "a stable derived uuid (uuid PK); supplying `id` pins it (and",
1815
+ "resets an int sequence past the max). All-or-nothing \u2014 mixing explicit and",
1816
+ "omitted `id` throws. A `system:false` PK is the author's to supply. Pinning is",
1817
+ "`seed`-only \u2014 `s.db.bulk.add` DROPS `id` unless `allowIdField: true`.",
1818
+ "**`use_xdo` storage mode.** Workspace setting (`registerWorkspace({ use_xdo })`,",
1819
+ "default `false`): `true` stores fields as JSON in the `xdo` column (+ `gin(xdo)` index),",
1820
+ "`false` real columns.",
1821
+ "Tables inherit it; override with `table({ useXdo })`; a merge refuses to switch a populated table.",
1822
+ "A **column `default` must stay within the BMP** \u2014 a 4-byte character (codepoint > U+FFFF,",
1823
+ "e.g. an emoji) is rejected at export; put such a value on an `input.<type>({ default })`.",
1824
+ "`input.*` mirrors `f.*` \u2014 every column type below is",
1825
+ "a legal input (scalars, files `input.image/video/audio/attachment`, `input.geo.*`,",
1826
+ "`input.vector(size)`, `input.tableRef(table)`, `input.object(children)`), plus",
1827
+ "`input.dbLink(table)` is the odd one: ONE entry that EXPANDS into one input per",
1828
+ 'COLUMN of the linked table, so read them by column name (`inp("email")`), never by',
1829
+ 'the entry\'s own name. `hidden: ["created_at"]` drops columns from that expansion.',
1830
+ "`input.list(element)` for arrays \u2014 wrap any element constructor, e.g.",
1831
+ "`input.list(input.text())` or `input.list(input.object({ id: f.int() }))`.",
1832
+ "**A file reaches a column in two steps.** `input.file` is the RAW upload (the request's",
1833
+ "multipart/base64 bytes) and cannot be written to a file column directly: store it first \u2014",
1834
+ '`s.storage.create_image({ as: "img", value: inp("avatar"), access: "public" })`',
1835
+ '(read the upload with `inp`; `ref("input.avatar")` spells a stack VARIABLE named',
1836
+ "`input` and fails the request with `Missing var entry: input`) \u2014 then write",
1837
+ '`ref("img")` into the `f.image()` cell with `s.db.add`/`edit`. `access` defaults to',
1838
+ '`"public"` (a guessable URL): pass `"private"` and hand out `s.storage.sign_private_url`',
1839
+ "results for anything user-scoped. `create_video`/`create_audio`/`create_attachment` are",
1840
+ "the same shape for the other file columns, each fed `input.file` (`input.image`/\u2026 400s an upload).",
1841
+ '**An empty file column is a stored null that `where` does not see as null.** `expr(col("file"), "!=", c.null())`',
1842
+ "matches EVERY row, and `= c.null()` matches none. Compare a key inside the file instead:",
1843
+ '`expr(col("file.path"), "!=", c.null())` keeps only the rows holding a file.',
1844
+ "**Typed inputs validate/coerce on bind, before your stack runs** \u2014 so reach for the",
1845
+ "specific type instead of hand-rolling checks. `input.email({ required: true })` rejects a",
1846
+ 'malformed address with a 400 (and trims; add `methods: ["lower"]` to downcase) \u2014 no',
1847
+ "`regex_matches` needed; `input.int`/`input.decimal`/`input.uuid`/`input.enum([...])`/`input.date`",
1848
+ "likewise reject or coerce bad input at the boundary. Drop to `input.text` + `s.precondition`",
1849
+ "only for rules no type expresses.",
1850
+ "\u26A0 `input.url` is NOT one of them \u2014 there is no engine `url` type, so it stores as `text`",
1851
+ "and validates NOTHING: a `javascript:`/`data:` URL type-checks, imports, and binds. When the value gets navigated",
1852
+ "to, check the scheme in the stack. It is INPUT ONLY \u2014 there is no `f.url` column.",
1853
+ "\u26A0 `input.timestamp` binds epoch MILLISECONDS. A number is taken as ms as-is, so epoch",
1854
+ "SECONDS (`date +%s`) bind in January 1970 with no error. An ISO-8601 string keeps its",
1855
+ 'offset but DROPS fractional seconds (`"\u2026T19:30:00.123Z"` binds `\u2026:00.000`). Send epoch ms',
1856
+ "(`Date.now()`, `getTime()`) whenever sub-second precision matters.",
1857
+ '\u26A0 `input.list` does NOT reject a non-array: one value binds as a ONE-item list (`"a"` \u2192',
1858
+ '`["a"]`), and a non-JSON string given to an `input.object` binds an object of its',
1859
+ 'children\'s DEFAULTS \u2014 so `input.list(input.object({...}))` given `"notalist"` binds',
1860
+ '`[{ name: "", \u2026 }]` with a 200. A list `min` does not catch it (that is one item). A string',
1861
+ "starting with `[` is parsed as JSON with NO element check at all. When the shape must hold,",
1862
+ "mark an object child `required: true` (the default-filled object then 400s with its param",
1863
+ "path) and check anything else with `s.precondition`.",
1864
+ '\u26A0 `s.precondition`\'s `error` must be a TAGGED value \u2014 `c.text("\u2026")`, not a bare string.',
1865
+ 'The engine falls back to the generic "Precondition failed." whenever it reads an empty or',
1866
+ "non-scalar message, and a bare string lands there, so the client never sees your text. The",
1867
+ "`error_type` \u2192 HTTP status mapping is correct either way; only the message is lost.",
1868
+ `\`error_type\` is how a stack sets a FAILURE status: ${preconditionStatusLine()}.`,
1869
+ "For any other status \u2014 and for a redirect \u2014 use `respond.*`, which is sugar over",
1870
+ "`s.util.set_header` (that statement reaches the status line): `respond.status(201)`,",
1871
+ "`respond.redirect(url, { status?: 301|302|303|307 })` (a TUPLE \u2014 spread it; a `Location`",
1872
+ "alone does NOT redirect), and `respond.header(name, value)`. Position in the stack does not",
1873
+ "matter, a later status wins, and a FAILING `precondition` still wins over anything set",
1874
+ "before it. \u26A0 `204` answers with an empty body whatever `response` says. \u26A0 `respond.status`",
1875
+ "REFUSES `308`/`425`/`451`/`102`/`103`: the platform writes those and does not act on them,",
1876
+ "so the response is 200 with nothing reported \u2014 use 301, 429 and 403 instead.",
1877
+ "Normalizing transforms run on bind too \u2014 put `trim`/`lower`/`upper` on the input's `methods`",
1878
+ 'so `inp("name")` reads already-normalized; don\'t reroll `var $x = inp("name")|trim` in the stack.',
1879
+ "\u26A0 `f.decimal`'s size depends on the table's layout; it takes no precision or scale option.",
1880
+ "On a normalized table (`useXdo: false`, the default) it is FIXED: 5 decimal places out",
1881
+ "of 14 total digits. `0.12345678` written and re-read is `0.12346` (`-0.000004` \u2192 `0`),",
1882
+ "and the rounding is silent at HTTP 200 \u2014 for a scalar `s.db.add`'s `as` output carries the",
1883
+ "STORED value, not the cell you sent, so comparing the two detects the loss; for a list",
1884
+ "(`array: true`) it is the unrounded list you sent, so only a re-read shows it. Rounding happens BEFORE",
1885
+ "the size is checked, so a magnitude needing 10 digits left of the point AFTER rounding 500s",
1886
+ "`ERROR_FATAL` `SQL Error: 0`, naming neither column nor value: `999999999.5` stores,",
1887
+ "`999999999.999999` does not. On a `useXdo: true` table it is a JSON number with no 5dp",
1888
+ "rounding (`0.123456789` reads back `0.123456789`). On either layout, for anything finer than 5dp",
1889
+ "store an integer count of the smallest unit (cents, satoshis) in an `f.int`.",
1890
+ "\u26A0 `f.int` is a SIGNED 64-bit column \u2014 `-9223372036854775808` to `9223372036854775807` \u2014",
1891
+ "and past the ceiling it CLAMPS instead of failing: `9223372036854775808` and",
1892
+ "`18446744073709551615` both store as `9223372036854775807`, at HTTP 200 with no error, so",
1893
+ "an overflowed count reads back as a plausible number. The response carries every digit, but",
1894
+ "`JSON.parse` (so `res.json()`) rounds past `9007199254740991` \u2014 read a count that large as",
1895
+ "text or BigInt. `c.int()` refuses an unsafe number literal, so pass the string.",
1896
+ ""
1897
+ );
1898
+ for (const ft of m.fieldTypes) {
1899
+ const stored = ft.stored !== ft.name.replace(/^geo\./, "") ? ` (stored \`${ft.stored}\`)` : "";
1900
+ const methods = ft.methods.length ? ` \u2014 methods: ${ft.methods.join(", ")}` : "";
1901
+ const ns = ft.inputOnly ? "input" : "f";
1902
+ const only = ft.inputOnly ? " \u2014 INPUT ONLY (no `f.` form)" : "";
1903
+ fields.push(`- \`${ns}.${ft.name}\`${stored}${methods}${only}`);
1904
+ }
1905
+ fields.push("");
1906
+ filters.push("## Filters", "");
1907
+ filters.push(
1908
+ "Attach to a value with `withFilters(v, fl.name(...))` \u2014 the value `filters[]`",
1909
+ "pipeline. Filters are passed spread (canonical); the array form",
1910
+ "`withFilters(v, [fl.a(), fl.b()])` is also accepted. Every filter is typed:",
1911
+ "the ones below carry named args, and the rest take NO arguments \u2014 call them",
1912
+ "`fl.<name>()`, and passing an argument is a compile error.",
1913
+ "A typed filter's declared argument list is an EXACT count, not a floor: passing more",
1914
+ "than it lists THROWS, in the type and at runtime. The extra argument used to ride into",
1915
+ "the filter's arg list, where the engine either ignores it or fails opaquely on a live",
1916
+ 'endpoint. `filter("name", \u2026)` is the untyped escape for a filter the catalog',
1917
+ "under-declares. Seven declared filters are exceptions the engine really does take more from, so they",
1918
+ "stay variadic: `concat` (trailing separator), `index_by` (trailing list flag), `get` (trailing variable",
1919
+ "map), `array_merge` and `array_merge_recursive` (any number of arrays), `jwe_encode` and",
1920
+ "`jwe_decode`. To send more than a zero-argument filter's `()` accepts, use the same",
1921
+ '`filter("name", \u2026)` escape. A filter with NO declared list takes nothing and is emitted',
1922
+ "`()` \u2014 except the variadic few listed under the catalog, which take arguments the",
1923
+ "catalog never declared and so are not arity-checked at all.",
1924
+ "A typed filter also accepts one object of NAMED arguments (`fl.add({ value: 1 })`); a key",
1925
+ "the filter does not declare throws rather than being dropped.",
1926
+ "A bare JS **scalar** \u2014 string, number or boolean \u2014 is accepted in ANY `fl.*` argument and",
1927
+ 'wrapped as the constant you would have written by hand (`fl.get("a.b", 0)` encodes',
1928
+ 'identically to `fl.get(c.text("a.b"), c.int(0))`). An object or array must still be built',
1929
+ "with `c.obj`/`c.array`. Any argument also takes a runtime value (`inp`, `ref`, a",
1930
+ "`withFilters` chain) \u2014 inside `obj()`/`expr()` an argument cannot carry its own filter",
1931
+ "chain, so compute it in a prior `set_var`. The arg types below name each argument's",
1932
+ "ENGINE type, not the JS type you may pass.",
1933
+ "To add to a numeric column (counter, stock, balance), use `s.db.increment`",
1934
+ "(`llms/statements-data.md`) \u2014 one atomic UPDATE. A pipeline read-modify-write is NOT",
1935
+ 'atomic (concurrent writers lose updates), and `col("clicks")` does NOT resolve to the',
1936
+ "stored value inside a `db.edit` `row` (it is `null`; `null + 1` aborts): `db.get` the row",
1937
+ '`as: "current"`, then `withFilters(ref("current.clicks"), fl.add(c.int(1)))`.',
1938
+ ""
1939
+ );
1940
+ const argFilters = m.filters.filter((fl) => fl.args?.length);
1941
+ for (const fl of argFilters) {
1942
+ const sig = (fl.args ?? []).map((a) => {
1943
+ const type = a.enum?.length ? a.enum.map((m2) => JSON.stringify(m2)).join("|") : a.type;
1944
+ return `${a.name}${a.optional ? "?" : ""}: ${type}`;
1945
+ }).join(", ");
1946
+ const ret = fl.result ? `: ${fl.result}` : "";
1947
+ const note = Object.hasOwn(FILTER_NOTES, fl.name) ? FILTER_NOTES[fl.name] : void 0;
1948
+ filters.push(`- \`${fl.fl}(${sig})${ret}\`${note ? ` \u2014 ${note}` : ""}`);
1949
+ }
1950
+ const zeroArg = m.filters.filter((fl) => !fl.args?.length && !fl.variadic).map((fl) => fl.name);
1951
+ const variadic = m.filters.filter((fl) => fl.variadic).map((fl) => fl.name);
1952
+ filters.push(
1953
+ "",
1954
+ `Zero-argument filters \u2014 call as \`fl.<name>()\`; an argument is a compile error: ${zeroArg.join(", ")}.`,
1955
+ "",
1956
+ `Variadic filters \u2014 they take arguments, but no declared list, so the count is not checked: ${variadic.join(", ")}.`,
1957
+ ""
1958
+ );
1959
+ const lambda = renderLambdaSection();
1960
+ lines.push("## Statements", "");
1961
+ lines.push(
1962
+ "Reachable through the `s` namespace: `s.<path>({...})`. Declarative statements",
1963
+ "take one typed args object (field names match the engine); every surface's field",
1964
+ "signature is in `llms/statements-catalog.md`. Specials (`[special]`) are",
1965
+ "hand-authored: control flow is below, and the rest are in the `llms/statements-*`",
1966
+ "topic files this doc's navigation names.",
1967
+ "Wrap an `input`-routed `value` field in `ignored(...)` to store it but SKIP it at",
1968
+ " runtime \u2014 the engine records `<name>:ignore` and the parameter falls back to its",
1969
+ " default. Not the same as an empty value, and not the same as omitting the field",
1970
+ " (which stores no entry at all). Mostly seen on a pulled workspace. A field written",
1971
+ " into `context` instead (the `s.math.*`/`s.text.append` mutation family) has no slot",
1972
+ " for the flag and THROWS rather than dropping it.",
1973
+ "Fields marked `value` take a `Value` (`c.*`/`ref`/`inp`); `comparison` takes an",
1974
+ "`expr(...)`. A `\u2192 as: <type>` suffix names what the statement's `as:` output var",
1975
+ "holds (curated, not exhaustive \u2014 absence means read the `[output]` flag and prose).",
1976
+ ""
1977
+ );
1978
+ lines.push(
1979
+ "### Specials \u2014 authored signatures",
1980
+ "",
1981
+ "Control flow & blocks (each nests a sub-stack; block specials name it `body`):",
1982
+ "",
1983
+ "- `s.set_var(name, value)` \xB7 `s.update_var(name, value)` \xB7 `s.return(value)` \xB7 `s.comment(text)` \u2014 positional.",
1984
+ '- **Every** statement takes `disabled?`/`description?` \u2014 annotations on the stack item, not args: `disabled: true` is Xano\'s "disable step" (kept in the stack, skipped at runtime), `description` the note beside it. Inline on object-arg factories; a trailing object on the positional ones (`s.set_var("x", v, { disabled: true })`).',
1985
+ '- **Statements with an `as`** also take `asFilters?` \u2014 `fl.*` filters on the RESULT as it binds, in order, same slot as `disabled`: `s.set_var("x", v, { asFilters: [fl.trim(), fl.lower()] })`. Saves a follow-up `set_var`. Throws without an `as`. The bound variable is RETYPED by the chain (`db.query` + `[fl.count()]` \u2192 `number`); filters whose result the engine declares as `any` (`get`, `set`, `json_decode`, \u2026) fold to `unknown`.',
1986
+ "- `s.conditional({ when, then, elif?, else? })` \u2014 if/elif/else. `when` is a condition (`expr`/`cmp`/`and`/`or`); `elif` is an ordered `[{ when, then }]` (each an else-if branch); `then`/`else` are `Statement[]`.",
1987
+ "- `s.for({ as, count, body })` \u2014 **count-bounded** loop (`as` is the index), NOT from/to; `count` is a `Value`, not a bare number.",
1988
+ "- `s.foreach({ as, list, body })` \u2014 iterate `list`; `as` is the current item.",
1989
+ "- `s.while({ when, body })` \u2014 `when` is a condition (`expr`/`cmp`/`and`/`or`).",
1990
+ "- `s.switch({ on, cases: [{ when, body, break? }], default? })` \u2014 multi-way branch on a subject `Value` `on`; each `case`'s `when` is a literal `Value` matched against `on` (NOT a comparison \u2014 use `s.conditional` for `<`/`>`/ranges). \u26A0 **Omitting `break: true` FALLS THROUGH** \u2014 the matched case also runs every LATER case body. Type-checks clean; only `export --strict` catches it.",
1991
+ "- `s.try_catch({ try, catch?, finally? })` \u2014 three `Statement[]` blocks.",
1992
+ "- `s.group(body)` / `s.util.post_process(body)` \u2014 take a `Statement[]` **positionally**.",
1993
+ "- `s.foreach_break()` / `s.foreach_continue()` / `s.foreach_remove()` \u2014 nullary loop control.",
1994
+ "- `s.expect.to_throw({ body, exception? })` \u2014 `body` is the statements expected to raise; `exception` is a `Value`, not a bare string.",
1995
+ ""
1996
+ );
1997
+ const stmtData = [
1998
+ "Array blocks (an `if`/`transform` is applied per item):",
1999
+ "",
2000
+ '- `s.array.map({ source, as?, transform? })` \u2014 `transform` is either a per-item `Value` expression (each item maps to that value) or a **record of values** (each item maps to an object with those keys), or a list of `{ key, value }` pairs, for a key computed per item or two rows sharing one. Use `ref("$this")` for the item and `ref("$index")` for its position. These are THIS statement\'s own bindings, in a value expression \u2014 not the JavaScript lambda contract (see **Lambda bodies**), which binds a different set per surface and is written with `lam.fn`.',
2001
+ "- `s.array.union({ source, with, as?, transform? })` \u2014 set-union two arrays. `with` is REQUIRED; omitted \u2192 `Unable to locate input: `.",
2002
+ "",
2003
+ "DB reads/writes (`table` is a def handle or name; `fieldName` defaults to the",
2004
+ "primary key `id`):",
2005
+ "",
2006
+ "- `s.db.get({ table, fieldName?, fieldValue, lock?, output?, as? })` \u2014 one row by field match; `output` restricts returned columns (and overrides column visibility \u2014 it can pull `internal` columns like a password hash).",
2007
+ "- `s.db.has({ table, fieldName?, fieldValue, as? })` \u2014 existence test.",
2008
+ "- `s.db.del({ table, fieldName?, fieldValue, as? })` \u2014 delete by field match.",
2009
+ "- `s.db.add({ table, row?, data?, output?, as? })` \u2014 insert; `row` is a partial keyed by column.",
2010
+ ' - A row CELL takes a tagged `Value`, a nested object of sub-keys, or a bare JS literal typed against that column: `row: { is_hidden: true, notes: "\u2026" }` encodes exactly as `{ is_hidden: c.bool(true), notes: c.text("\u2026") }`. The tag comes from the COLUMN, not the literal \u2014 `10` on an `f.decimal()` column is `const:decimal`, not `const:int` \u2014 so a literal contradicting its column is a compile error on a `f.*`-schema table (`{ is_hidden: "yes" }` on an `f.bool()` column) and throws at encode on a raw-`ColumnDef[]` one. An `f.enum()` column keeps its member union. A column with no literal form \u2014 obj/json/list/geo/vector/file \u2014 still needs `c.obj`/`c.array`.',
2011
+ " - `null` is accepted on EVERY column, including ones that refuse every other literal, and encodes `const:null` \u2014 a write OF null, not the same as omitting the key. A column's `nullable` is not consulted at encode; the engine refuses a null it forbids.",
2012
+ " - Omitting a key on `add` writes the column's type default \u2014 declared `default` if set, `[]` for a list, `{}` for obj/json, else `null`. That `null` is EMITTED, not what the row holds: the engine applies the column's nullability, so a `nullable` column keeps `null` and a non-nullable one lands on its type's zero value (`\"\"`, `0`, `false`). Set the cell when the stored value matters. On `edit` an omitted key keeps its stored value.",
2013
+ " - An `f.password()` cell takes the PLAINTEXT \u2014 the column hashes on write, so a pre-hashed value, or a hashing filter on the cell, stores a hash of a hash that `security.check_password` can never match.",
2014
+ " - A `table({ seed })` cell hashes the same way: the import writes the plaintext through the column's own rules, so a seeded credential matches under `security.check_password` exactly as an added one does. Demo accounts work as fixtures \u2014 the usual caution about seed data applies, since the plaintext sits in the repo.",
2015
+ "- `s.db.edit({ table, fieldName?, fieldValue, row?, data?, output?, as? })` \u2014 update by field match.",
2016
+ "- `s.db.patch({ table, fieldName?, fieldValue, data, output?, as? })` \u2014 merge a partial. \u26A0 Unlike `db.edit`'s `row`, `data` is a single object `Value` \u2014 write `obj({ unread: c.int(0) })`, not the column-keyed record `row` takes.",
2017
+ " On these three, `output` restricts the columns of the RETURNED row only \u2014 it does not change",
2018
+ " what is written. Not offered on `db.del`/`db.has` (their result is a scalar) or on",
2019
+ " `db.add_or_edit` (no output envelope).",
2020
+ "- `s.db.add_or_edit({ table, fieldName?, fieldValue, row?, data?, as? })` \u2014 upsert.",
2021
+ "- `s.db.query({ table, where?, additionalWhere?, bind?, sort?, paging?, external?, returnType?, distinct?, eval?, output?, lock?, addon?, as? })` \u2014 search.",
2022
+ " - `where` / `additionalWhere` \u2014 `expr(...)`, an `expr[]` (ANDed), or a raw `Value`. Rides `context.search`.",
2023
+ " - \u26A0 `ignoreEmpty` DROPS the predicate when the operand is empty (not zero rows). On `in` an empty list returns the UNFILTERED set, so never scope to permitted ids with it; a `bulk.delete` only it scopes throws.",
2024
+ " - For the full operator set use `cmp(left, op, right, { ignoreEmpty? })` \u2014 `op`: `in`/`not in`/`like`/`ilike`/`between`/`contains`/`includes`/`overlaps`/`@>`/`~`/`search`/\u2026 plus the `expr` comparisons. Database-only \u2014 a runtime condition takes the `expr` set only.",
2025
+ ' - \u26A0 `like`/`ilike` take the operand as the PATTERN, verbatim: a bare term matches only an exact whole-string equal, and the endpoint answers HTTP 200 with zero rows \u2014 nothing reports a problem, so a search box that matches nothing ships. For substring matching use `includes`/`not includes`, which wrap the operand in `%\u2026%` themselves and match case-INSENSITIVELY. Prefer them over a hand-built `"%" + term + "%"`, which is non-empty even for an empty term and so defeats `ignoreEmpty`; `includes` composes with it. `contains`/`@>`/`overlaps` are JSON/array containment, not text \u2014 on a text column they 400 `ParseError: Invalid value for param`.',
2026
+ " - Compose nested boolean logic with `and(...)` / `or(...)` groups (also available on `addon()` `where`).",
2027
+ " - An operand may be a bare value (`col`/`inp`/`ref`/`auth`/`c.*`) OR a **filtered** value (`withFilters(...)`) inline \u2014 the engine compiles the string and arithmetic filters (`trim`, `concat`, `upper`, `lower`, \u2026) into the SQL.",
2028
+ ' - \u26A0 The REQUEST-TIME timestamp filters have no SQL form and kill the request with a bare fatal naming nothing: `epochms_transform`, `epochms_add_ms`, `epochms_add_secs`, `epochms_date`, `epochms_from_format`. For a relative cutoff use the SQL-side family instead (`qf.epochms_add_day(7)`, `qf.epochms_sub_month(1)`, `qf.epochms_year("UTC")`, \u2026) \u2014 or compute it in an earlier `s.set_var` and `ref()` that. `export --strict` reports it; a bare `c.now()` operand is always fine.',
2029
+ ' - `bind: [{ table, as?, join?, where? }]` \u2014 joins (`context.bind[]`). `join` defaults to `"inner"`. `as` defaults to the table name; two joins to the same table need distinct aliases.',
2030
+ ' - \u26A0 In `where`/`sort`/`eval` a JOINED column takes a dotted path (`col("team_row.id")`); THIS query\'s own columns stay **bare** (`col("team")`). Qualifying your own by table name needs `tableAlias` (same rule as `aggregate`) \u2014 without it the engine reads the operand as text and 400s `ParseError: Invalid value for param` naming the OTHER operand, so it throws at export instead.',
2031
+ ' - `bind: [{ table: team, as: "team_row", join: "left", where: expr(col("team"), "=", col("team_row.id")) }]`',
2032
+ ' - \u26A0 A join does NOT put the joined table\'s columns on the returned row \u2014 with or without a `bind`, a row is the QUERIED table\'s columns, which is what `InferResponse` types. There is no `row.team_row`. To read a joined column, PROJECT it with an `eval` whose `name` is the dotted path: `eval: [{ name: "team_row.name", as: "team_name" }]` puts `team_name` on the row and on the inferred type. A bare `name` there is `Unsupported parameter reference` at runtime (it qualifies to the base table), and a dotted joined column in `output` is dropped with no error.',
2033
+ ' - `returnType` \u2014 `"list"` (default) | `"single"` | `"count"` | `"exists"` | `"stream"` | `"aggregate"`. Drives `context.return.type` AND the `InferResponse` shape: `count`\u2192`number`, `exists`\u2192`boolean`, `single`\u2192`Row|null`, `stream`\u2192`Row[]` (pageable, no envelope), `list`\u2192`Row[]`/envelope, `aggregate`\u2192rows keyed by the `aggregate.group`/`eval` aliases. \u26A0 A bare `count` of ZERO serializes as an EMPTY body, not `0` \u2014 a client parsing JSON gets a parse error on the one result it most needs to handle. Wrap it: `response: { count: ref("n") }`.',
2034
+ ' - `eval: [{ name, as, filters? }]` \u2014 computed columns (`context.eval[]`). Each `as` grafts onto the row as an `unknown` key in `InferResponse`; shadowing a real column throws. Write `name` **bare** (`"embedding"`) \u2014 it is alias-qualified on emit exactly like `aggregate` (a bare eval name is `Unsupported param format` at runtime), and the statement declares the alias it used. An `as` alias is `sort`able in the SAME query.',
2035
+ ' - An `eval`/`sort`/`where` filter pipeline compiles to **SQL**: `trim`/`lower`/`concat` work there too, plus a DIFFERENT registry than `fl.*`: the vector family, geo `distance`/`within`/`covers`, `search_rank`, the timestamp family, the aggregators. Build these with **`qf.*`** \u2014 `filters: [qf.vector_cos_distance(inp("q"))]` \u2014 which emits the same `{ name, arg }` step with the name, the argument COUNT and any enumerated argument checked at the call site. `qf` carries what the engine registers, including facts no name list holds: the per-part extractors take a timezone (`qf.epochms_month("UTC")`), `qf.round`\'s precision is required, `qf.time`\'s part is an enum. `QUERY_FILTER_NAMES`/`QUERY_AGGREGATE_NAMES` list the set; the raw `{ name, arg }` form still works for anything `qf` does not carry (`vector_distance`, which has no registered engine class).',
2036
+ ' - **Vector similarity search** \u2014 the ONLY way to query an `f.vector` column (no `SearchOp` does distance). `eval: [{ name: "embedding", as: "distance", filters: [{ name: "vector_cos_distance", arg: [inp("q")] }] }]` + `sort: [{ sortBy: "distance", dir: "asc" }]` ranks in the DATABASE over the column\'s index. Match the filter to the index `op` (`vector_cos_distance`\u2194`vector_cosine_ops`, `vector_l2_distance`\u2194`vector_l2_ops`, `vector_l1_distance`\u2194`vector_l1_ops`, `vector_inner_product`\u2194`vector_ip_ops`); `vector_cos_similarity` is the inverse, so sort it `desc`. The same filter on a `where` operand cuts off BY distance instead of by row count.',
2037
+ ' - `aggregate: { group?, eval?, sort?, paging? }` (with `returnType:"aggregate"`) builds `context.return.aggregate`. `group`/`eval` are `{ name, as, filters? }`, an aggregator like `sum`/`count` riding `filters`. Some aggregators resolve ONLY here, not in a runtime value pipeline: `count_distinct`, `median`, `to_list`/`to_distinct_list` (each with `_asc`/`_desc`), and `vector_distance`.',
2038
+ ' - \u26A0 Write each `name` as a **bare** column (`"status"`). It is alias-qualified to `"<alias>.status"` on emit \u2014 the engine rejects an unqualified column in an aggregate with `Unsupported param format`. An already-dotted `name` (a `bind`ed/joined column) passes through.',
2039
+ " - The alias it qualifies WITH is `tableAlias` when you set one, otherwise the table's name \u2014 and the statement DECLARES that alias (`dbo.as`) so the qualified name resolves. Nothing to do by hand; a bare `name` is the form to write.",
2040
+ ' - `sort: [{ sortBy: <col>, dir?: "asc"|"desc"|"rand" }]` and `paging: { page?, per_page?, offset?, totals?, metadata?, search?, sort? }` ride `context.return.list`.',
2041
+ " - \u26A0 `paging` with a page/per_page/offset field and `metadata` on (the DEFAULT) wraps the result in an envelope `{ items: Row[], curPage, nextPage, prevPage, offset, perPage, itemsReceived }` \u2014 plus `itemsTotal`/`pageTotal` when `totals: true` \u2014 instead of a bare `Row[]`. `InferResponse` reflects it. `paging: { metadata: false }` keeps the bare array.",
2042
+ ' - \u26A0 Under the ENVELOPE, `output` selects from the envelope, not the row: write `output: ["itemsReceived", "curPage", "items.id", "items.title"]`. A bare column list matches no envelope key, so every key is dropped and the response is `[]` at HTTP 200 with no error (and `ref("<var>.items")` then fails on a var that is gone). With `metadata: false` the statement binds the rows, so bare columns are the right form there. `export()` warns on a row-rooted selection under an envelope; `--strict` fails it.',
2043
+ " - Read `nextPage` (`number|null`) as the typed has-next signal.",
2044
+ ' - **Input-bound paging:** `page`/`per_page`/`offset` also accept a `Value` (`inp("page")`), riding `context.simpleExternal` while the static block stays the engine gate (`enabled:true`). `paging.search`/`sort` are `Value` dynamic overrides.',
2045
+ " - A `search`/`sort`-only `paging` (no numeric field) does NOT paginate.",
2046
+ " - `external: { value, permissions? }` \u2014 the classic whole-config blob (forces the gate on). It falls back to input-bound `paging` when it resolves empty, so supplying both is valid.",
2047
+ ' - `distinct` \u2014 `"auto"` (default) | `"yes"` | `"no"`, riding `context.return.<list|stream>.distinct`.',
2048
+ "- `s.db.truncate({ table, reset?, as? })` \xB7 `s.db.schema({ table, path, as? })`.",
2049
+ '- `s.db.direct_query({ sql, responseType?, args?, parser?, as? })` \u2014 `sql` is a **raw string** (not a `Value`); binds go in `args: Value[]`. `parser: "template_engine"` renders the body as a template first \u2014 how a query interpolates a column or table name a bound arg cannot carry; omit it for the default.',
2050
+ " - Template placeholders are Twig over the request scope: `{{ $input.name }}` for an input, `{{ $var.name }}` for a stack variable. \u26A0 A BARE `{{ name }}` renders as the empty string \u2014 HTTP 200, no error, a query that silently ran with a blank where the value belonged. A bound `?` arg carries a VALUE without the template at all.",
2051
+ " - \u26A0 A table's PHYSICAL name is **not stable across deploys**. A deploy is a full replace, so every table is created afresh and the id in its name moves every time \u2014 the same unchanged project redeployed three times gave one table three different names. Never store, cache, hardcode or fixture one: resolve it from `information_schema` inside the same request that uses it.",
2052
+ "- `s.db.external.<engine>.direct_query({ sql, connectionString, responseType?, args?, parser?, as? })` \u2014 same shape against an EXTERNAL database; `<engine>` is `postgres`/`mysql`/`mssql`/`oracle`/`snowflake`. `connectionString` is a `Value` \u2014 reach for `env(...)`, not a literal \u2014 stored as `context.connection_string_flex`. A bare string stores the older `context.connection_string` instead (an env-var name unless it looks like a URL); each form round-trips as itself.",
2053
+ "- `s.db.transaction({ body, as? })` \u2014 run a `Statement[]` atomically. `as` binds whatever the block returned.",
2054
+ "- `s.db.bulk.add({ table, items, allowIdField?, as? })` / `s.db.bulk.update` / `s.db.bulk.patch` \u2014 `items` is an array `Value`.",
2055
+ " - \u26A0 `bulk.add` **drops `id` on every row unless `allowIdField: true`** (silently, next sequence value instead) \u2014 the opposite of `seed`, where `id` pins. Rows referenced by a foreign key need `allowIdField: true`; literal `items` carrying `id` without it throw. `bulk.update`/`patch` keep `id` (their match key).",
2056
+ ' - \u26A0 **`bulk.update` is a whole-row REPLACE: every column an item OMITS is zeroed** (`""`/`0`/`null`), HTTP 200, no error \u2014 `{ id: 7, status: "done" }` blanks the rest of row 7. **Use `s.db.bulk.patch`** for the partial write "update these rows" means. `export()` warns on a STATIC `items` missing columns (`--strict` fails); a `ref`/`inp` `items` is uninspectable.',
2057
+ "- `s.db.bulk.delete({ table, where?, allRows?, as? })` \u2014 deletes rows by a `context.search` filter. `where` is the same surface as `s.db.query` (`expr(...)`/`cmp(...)`, `and(...)`/`or(...)` groups, an array of those ANDed, or a raw `Value`) and encodes through the identical `{expression:[\u2026]}` search shape. \u26A0 A filter that constrains nothing (missing, empty, or `x OR and()`) deletes **every** row, so it **throws**: pass the filter, or `allRows: true` for a deliberate wipe (both together also throw). `allRows` emits the empty search the engine requires and returns the deleted count; reach for `s.db.truncate({ table, reset: true })` when the id sequence should restart too.",
2058
+ '- `s.db.increment({ table, where, fieldName, value, returnType?, output?, addon?, as? })` \u2014 adds `value` (a number or `Value`; negative decrements) to one `int`/`decimal` column on every row `where` matches, in ONE atomic locked UPDATE. **Use it for counters, stock, balances and tallies**: `db.get` \u2192 math \u2192 `db.edit` loses updates under concurrency, and so do `db.bulk.patch`/`update`. A guard in `where` (`expr(col("stock"), ">=", c.int(1))`) is re-checked under the row lock. `returnType: "count"` binds the changed-row count instead of the rows, and then takes no `output`/`addon`.',
2059
+ " - \u26A0 A `where` that constrains nothing (missing, empty, or `x OR and()`) changes NO rows and **throws**. A null stored value counts as 0. A fractional `value` on an `int` column, a non-numeric or list column, the `id` key, and a result that overflows the column are rejected with no rows changed.",
2060
+ ""
2061
+ ];
2062
+ const stmtRuntime = [
2063
+ "Runtime behavior (what the `as:` output holds, and misses):",
2064
+ "",
2065
+ "- `db.get` binds **`null`** when no row matches (it does NOT throw) \u2014 so the output is `InferRow<typeof table> | null`; null-check it. On a hit it binds the **full row**. (`db.has` is the boolean existence test.)",
2066
+ "- `db.edit` binds the **full, post-mutation row**. `db.add` binds the **full inserted row**, including the auto-assigned `id` and `created_at`. So `InferRow<typeof table>` is the right response type for those two. `db.del` **binds `null`**, not the deleted row.",
2067
+ '- Unlike `db.get`, `db.edit` and `db.del` **throw** `NotFound` (HTTP 404) when no row matches the field. `db.add` throws on a unique-constraint violation: HTTP 500 `ERROR_FATAL` "Duplicate record detected\u2026", earlier writes kept. No error type is 409. `s.try_catch` catches it \u2014 and `s.precondition`/`guard.*` failures, answering 200 (`caught("code")`: `ERROR_CODE_NOT_FOUND`, \u2026) \u2014 so re-raise with an `s.precondition` in `catch` to keep a status. No partial unique index: NULLs never collide, so null the key (or delete the row) to free it.',
2068
+ "- **`InferResponse<typeof query>`** derives an endpoint's response type. It resolves object-literal responses to those keys; a `response: ref(\"x\")` returning a variable bound by a TOP-LEVEL db op on a `table()` resolves to that op's result:",
2069
+ " | statement | resolves to | on a miss |",
2070
+ " |---|---|---|",
2071
+ " | `db.add` / `db.edit` / `db.patch` / `db.add_or_edit` | `Row` (the full written row, non-nullable) | throws \u2014 `NotFound`/404 for `edit`/`patch`, a unique-constraint error for `add`; `add_or_edit` upserts and never misses |",
2072
+ " | `db.get` | `Row \\| null` | binds `null` rather than throwing |",
2073
+ " | `db.query` / `db.bulk.patch` | `Row[]` | \u2014 |",
2074
+ " | `db.has` | `boolean` | \u2014 |",
2075
+ " | `db.bulk.delete` | `number` (count) | \u2014 |",
2076
+ ' | `db.increment` | `Row[]` (updated rows), or `number` with `returnType: "count"` | `[]` / `0` |',
2077
+ " | `db.del`, `db.bulk.add`/`bulk.update`, raw `direct_query` | `unknown` (the engine leaves them untyped) | \u2014 |",
2078
+ ' - A `get`/`query` `output: [...]` selection narrows to a `Pick` (still `| null` for `get`). A dotted `ref("row.col")` into a `db.get` row projects that column carrying the `| null` (\u2192 `Col | null`).',
2079
+ " - A value reshaped by a lambda, or a variable built by control flow, also resolves to `unknown`.",
2080
+ ' - A CALL binds the TARGET\'s response type: `s.function.call`/`s.function.run`/`s.api.call`/`s.tool.call` given a def HANDLE brand their `as` with `InferResponse` of the target, so `ref("out.field")` types to that field. What the target resolves to is what propagates \u2014 declare `responseShape` on the TARGET to fix every caller at once. A target named by STRING has no def to read and stays `unknown`; an async `s.function.run` binds its job id (`string`), not the result.',
2081
+ " - In an object-literal response the KEYS are always known; a `c.*` constant VALUE types like an `s.set_var` of it (`{ success: c.bool(true) }` \u2192 `{ success: boolean }`).",
2082
+ ' - A NESTED member resolves to any depth by these same rules, in either spelling \u2014 `{ user: obj({ id: ref("row.id") }) }` and the raw literal `{ user: { id: ref("row.id") } }` both derive `{ user: { id: Col | null } }`. A raw scalar member types itself (`{ count: 3 }` \u2192 `number`).',
2083
+ " - Runs on every response-bearing kind \u2014 `query`, `defineFunction`, `realtimeMessage`, `tool`, `middleware`, and the response-bearing triggers \u2014 each of which also accepts `responseShape`. A trigger builds its stack and response through CALLBACKS (`stack: (t) => [...]`), and the trace follows through them.",
2084
+ " - Close any `unknown` by declaring `responseShape` on the def (`responseShape: null as InferRow<typeof t> | null`) \u2014 the declaration ALWAYS overrides derivation. It is available on EVERY kind listed above, `query` included.",
2085
+ " - The shapes that never derive, and the one line that closes each: rows built in `s.lambda` (JS is opaque to the walk) \u2192 `responseShape: [] as Row[]`; a var bound ONLY inside branches \u2192 `responseShape` (a branch/case/loop/`try`/`catch` re-binding unions in; one in EVERY branch replaces); a call whose target is named by STRING \u2192 give the TARGET a `responseShape` (a handle then carries it) or declare it here. Use `responseShape` as the FIRST move here.",
2086
+ ' - An `s.set_var` holding a `c.*` CONSTANT brands its binding with that constant\'s JS type, so `ref("n")` after `s.set_var("n", c.int(1))` resolves to `number` (widened: `c.bool(false)` \u2192 `boolean`; `s.update_var` re-binds). A filter chain \u2014 the value\'s own or `asFilters` \u2014 then retypes it (`[fl.to_int()]` \u2192 `number`). A bare `ref`/`inp` stays `unknown`.',
2087
+ ' - \u26A0 A hand-written `s.precondition` does not narrow the STATIC type. One (or a conditional) proving a `db.get` row non-null leaves `InferResponse` at `Row | null`, because the walk reads the stack\'s shape and not its control flow. `guard.found("row")` DOES narrow: it emits the same non-null check (404) and drops `| null` from `row` \u2014 whole, dotted, and addon grafts alike \u2014 after it. `guard.owner` narrows the same way. Reach for `responseShape` only when neither fits.',
2088
+ ' - \u26A0 A `resultStrategy: "replace"` middleware attached `post` reshapes the endpoint\'s output at runtime, which the static walk cannot see. Declare `responseShape` when a post middleware rewrites the response. A `merge` one makes a LIST response an index-keyed object; its keys are not inferred.',
2089
+ " - \u26A0 **Spreading a `Statement[]` helper into a stack kills the whole walk.** The trace needs the stack's TUPLE type, so `...myHelper()` where the helper returns `Statement[]` widens it and EVERY `as` in that stack \u2014 including ones declared after the spread \u2014 stops resolving. The response then types as `StackTupleWidened`, whose name says so. Fix: return `statements(s.a(...), s.b(...))` from the helper (a const-generic identity export \u2014 the tuple survives the spread). A helper that builds its array in a LOOP cannot be a tuple; declare `responseShape` there.",
2090
+ "- **Addons** enrich returned rows. `db.query`/`get`/`add`/`edit`/`patch` accept `addon: [{ addon, as, input?, output?, children? }]`; `db.add_or_edit`/`del`/`has`/`truncate` take no `addon`.",
2091
+ ' - `addon` is the target (name or def handle). `as` is the destination on the row \u2014 a bare alias (`"_user"`) or a dotted `offset.alias`, authored relative to a row. Under a metadata paging envelope the `items[]` offset is prefixed automatically; writing it yourself is tolerated and not double-prefixed.',
2092
+ " - `input` maps addon inputs \u2014 bind a parent-row column with `out(col)`. `output` restricts addon columns. `children` nests addons.",
2093
+ " - The DEFINITION side \u2014 the `addon({...})` factory and `registerAddons` \u2014 is a def shape in `llms/kinds-core.md`.",
2094
+ " - Attaching a typed `addon({ table, output })` handle merges its alias (the last `as` segment) onto the row in `InferResponse`: `{cols} | null` for `single`, `{cols}[]` for `list`, `number` for `count`, `boolean` for `exists`, and for `aggregate` an array keyed by the `group`/`eval` aliases (`unknown` values; `unknown` when neither is declared).",
2095
+ " - An attachment-level `output` narrows an object/array graft further. A bare-NAME reference grafts `unknown` \u2014 narrow it at the call site.",
2096
+ " - \u26A0 An alias that shadows an existing column on the queried table throws at build time; rename with a `_` prefix.",
2097
+ "- **Middleware attachment** runs a reusable `middleware({...})` before/after a host's own stack. Distinct from `s.middleware.call` (inline invoke).",
2098
+ " - Attach with the host's `middleware: { pre, post }` field on `query`/`function`/`task`/`tool`/`apiGroup` (NOT triggers): each phase is an ordered list of middleware refs (def handle or name), or `{ middleware, active: false }` to keep an entry disabled.",
2099
+ " - Providing a phase **overrides** it (sets the stored `pre_customize`/`post_customize` flag); omitting a phase **inherits** the parent tier's chain \u2014 the engine resolves Query \u2192 API Group \u2192 Workspace at request time (override, not merge; the API-Group tier applies to queries \u2014 functions/tasks/tools have no API-group binding and inherit straight from the workspace).",
2100
+ " - Prefer a def handle over a bare name when the middleware pins an explicit `guid`.",
2101
+ " - `pre: middleware.clear()` (an empty list) overrides with nothing \u2014 stop inheriting.",
2102
+ " - Workspace-level defaults are the terminal tier: `workspaceConfig({ middleware: { query: { pre }, function, task, tool } })` emits the flat `{host}_{phase}` map (no `_customize` flags) \u2014 setting it replaces the whole workspace map, so unlisted hosts are cleared; omit the field to leave existing workspace middleware untouched.",
2103
+ "- **Middleware request context.** A `pre` middleware runs **after** auth resolution, so `auth()` is available inside the middleware when the host is authenticated (its `auth` names an auth table); on a public host there is no caller identity for it to resolve.",
2104
+ ' - This matters for the canonical use \u2014 a rate limit keyed by `auth("id")`: on an authenticated endpoint the bucket is per-user, but attach the same middleware to a public endpoint and `auth()` cannot resolve, so the request FAILS with a 403 on the first call, under `max`, with the host never running. Key off `sys.remoteIp()` on a public host.',
2105
+ " - To catch that, `export()` **warns** (never blocks) when a middleware whose stack references `auth()` reaches a host where `auth()` may be null \u2014 a `query` with no auth table, a `task` (scheduled, never authenticated), or a `function`/`tool` (whose auth is caller-dependent). An authenticated query (its own `auth` table set) is skipped.",
2106
+ " - Inherited attachment (API group, workspace tier) warns too.",
2107
+ '- **Rate-limit recipe (the canonical middleware).** Author it with `s.redis.ratelimit` and a **composite key** built via the filter chain \u2014 `"prefix" + auth("id")` does not exist, you build the key: `middleware({ name: "write_rl", exceptionPolicy: "rethrow", stack: [ s.redis.ratelimit({ key: withFilters(c.text("rl:write:"), fl.concat(auth("id"))), max: c.int(10), ttl: c.int(30), error: c.text("Too fast.") }) ] })`.',
2108
+ ' - `exceptionPolicy` defaults to `"rethrow"`, which is what makes a tripped limit abort with HTTP 429; `"silent"` would let the over-limit request through. Without `error` it never trips: it binds `false` and the request runs on (`export()` warns).',
2109
+ " - Attach it with `middleware: { pre: [writeRl] }` on an **authenticated** host; on a public host key off `sys.remoteIp()`.",
2110
+ " - **Shared-bucket rule:** co-attaching one middleware object to N hosts means all N share the *same* key \u21D2 *one* counter \u2014 `max: 10` is a global per-user budget across them, not 10-per-host. Vary the key (fold in the host/action name) for an independent limit per host.",
2111
+ '- **Middleware `exceptionPolicy`** governs what a **throw** in the middleware stack does to the request. `"rethrow"` is the **default** \u2014 the throw aborts the request and surfaces the authored `error`/status (a tripped `s.redis.ratelimit` \u2192 HTTP 429); the `post` chain still runs. `"silent"` swallows the throw, so a guard set to it is **not enforced** \u2014 advisory middleware only. `"critical"` is `"rethrow"` plus skipping the `post` chain.',
2112
+ "",
2113
+ '- **Request history** controls per-object execution capture (the request/task/trigger debugger). Authored as a single scalar `history` field on any primitive: `false` off, `true` on at the default capture depth, a number = capture depth (how many statement executions are recorded per history record \u2014 NOT record retention), `"all"` unlimited.',
2114
+ " - **Omit `history` to inherit** \u2014 the engine resolves object \u2192 container \u2192 workspace at request time (a query inherits from its API group, a tool from its toolset envelope, everything else straight from the workspace). Any authored value stops inheriting for that object.",
2115
+ " - Per-kind defaults (when inheriting): query/task/tool capture ON, function/trigger/middleware OFF; default depth 100.",
2116
+ " - Container tiers are authorable too \u2014 `apiGroup({ history })` sets the `query_*` default its queries inherit, and an agent/mcp_server/toolset `history` sets the `tool_*` default its tools inherit.",
2117
+ " - Workspace-level defaults are the terminal tier: `workspaceConfig({ history: { query, function, task, tool, trigger, middleware } })` emits the flat `{objType}_enabled`/`{objType}_limit` map (no inherit flag) \u2014 setting it is wholesale (unlisted types fall back to their engine default), so declare every default you want to keep; omit the field to leave existing workspace history untouched.",
2118
+ "",
2119
+ '- **Workspace environment variables** set a tenant\'s env vars through the workspace object: `workspaceConfig({ env: { STRIPE_KEY: "", APP_BASE_URL: "" } })`, passed to `registerWorkspace()` \u2014 unattached, it declares nothing. Author them as a name\u2192value MAP, declaring each NAME with an EMPTY value. Read a var back with `env("NAME")` (\u2192 `$env.NAME`), which compiles to tag "setting" with the plain name.',
2120
+ " - Values are SECRETS and do NOT belong here: they live in `xano/.env`, which is gitignored and survives a `xanosdk pull`, and every command that compiles a bundle reads it with no flag. Fill it by hand from `xano/.env.example`, or run `xanosdk env pull`. CI passes `--backend-env-file <path>` or `--env-var KEY=VALUE` instead. A compiled bundle carries the values in cleartext \u2014 don't commit one.",
2121
+ " - What a deploy sends is the RESOLVED map (the declared names, then ONE file \u2014 `xano/.env`, or `--backend-env-file <path>` which REPLACES it, never both \u2014 then `--env-var`), not the literal object written here. `deploy` REPLACES the tenant's env with it, so a declared name with no value anywhere REFUSES the deploy rather than clearing the live value \u2014 fill the name in rather than putting the value back in source, and reach for `--allow-empty-env=NAME` only when clearing is what you mean.",
2122
+ "",
2123
+ "- **Workspace documentation** gates the workspace's hosted docs: `workspaceConfig({ documentation: { require_token: true } })`. `require_token: true` IS the declaration that a token exists; the value lives in `xano/.secrets.json`, keyed by the object it gates rather than by a name, and a literal `token` FAILS the export. It is NOT a backend env var \u2014 `env()` cannot read it. Other members are carried verbatim.",
2124
+ " - \u26A0 OMIT the block and NO `documentation` key is emitted, leaving the target's live block as it is; the legacy `realtime` block follows the same rule. An `apiGroup`'s `documentation` follows the OPPOSITE rule \u2014 see its def shape.",
2125
+ " - A declared gate with no token emits no block at all and REFUSES a `deploy`; `--allow-empty-doc-token=<scope>` clears that gate deliberately (scope = `workspace`, or an API group's NAME). `xanosdk pull` writes `xano/.secrets.json`, and `--no-secrets` writes none.",
2126
+ " - A merge (`deploy --to`) is ADD-ONLY, matched BY NAME \u2014 it creates names the target lacks but does NOT update or remove ones it has, so the target's value wins. Two gaps: a release carries no env values, so a NEW name reads null after `promote`/`tenant deploy` until `env set` (both name it first) \u2014 and a `--replace` deploy rebuilds it so every env var the target holds that the config does not declare is DROPPED (named, and confirmed, before it runs). Omit `env` entirely to leave existing env untouched. The separate `settings` field is a plain object.",
2127
+ ""
2128
+ ];
2129
+ const lock = [
2130
+ "## Lock file",
2131
+ "",
2132
+ "`xano.lock` pins each object's guid and each api-group/toolset canonical, so renames",
2133
+ "stay renames (guids otherwise derive from `(type, name)`; a query's from `(api group,",
2134
+ "verb, name)`). Committed. A composed entry is keyed by that identity \u2014",
2135
+ "`query:<group>|<verb>|<name>`, `channel:<server>|<path>`, `message:<server>|<path>|<name>`",
2136
+ "\u2014 its guid's seed; a legacy `query:<name>` is migrated by the next locked export.",
2137
+ "",
2138
+ "- `xanosdk lock rename <kind> <old> <new>` \u2014 `kind` is the SDK kind (`table`, `apiGroup`) or stored key (`dbo`).",
2139
+ " Run it when `export` names it after a rename in code (it prints the command); the next export emits the original guid under the new name.",
2140
+ " Reads the `index.ts` beside the lock (or `--entry`); refused while `<old>` is still exported. Composed names go in full; renaming a parent moves them.",
2141
+ "- `xanosdk lock prune <entry-file> [keys\u2026] --yes` \u2014 drops orphaned entries. Finding orphans",
2142
+ " RUNS the entry's module scope (env assertions included); `--identity-only --yes <key>\u2026`",
2143
+ " prunes named keys (`table:users`) with no evaluation and no orphan check.",
2144
+ "- `xanosdk lock import <live-bundle.json> [--yes]` \u2014 seed the lock from a `workspace export`",
2145
+ " to take over an existing workspace (a compiled bundle is refused).",
2146
+ "- Every lock subcommand accepts `--lock=<path>`; from outside the lock's directory pass it.",
2147
+ "- Merge conflict: keep both sides; two keys on one guid \u2192 `lock rename` onto the kept",
2148
+ " name, or `lock prune --identity-only --yes` the other.",
2149
+ "- Programmatic use: call `seedLockOverrides(readLockFile(path))` BEFORE importing any def",
2150
+ " module \u2014 references bake guids at import time; `writeBundle` refuses an unseeded lock.",
2151
+ "",
2152
+ "What writes the lock: `export`/`deploy`/`release`/`preflight` of an ENTRY FILE update it;",
2153
+ "`compile`/`paths` only READ. `--no-lock` builds",
2154
+ "without one (names derive guids, the instance invents public URLs, no `--prune`);",
2155
+ "REFUSED over a parseable lock. A DEPLOY or a RELEASE writes back only `landed`: what",
2156
+ "this project LANDED on each destination (ephemerals: `.xano/ephemeral.json`). `--prune` deletes only that \u2014 never a merely",
2157
+ "exported name; no record there, no prune until one landing (commit it). A `deploy` (`--replace`",
2158
+ "too) PRESERVES the archive's guids, so the lock still describes the rebuilt",
2159
+ "workspace. CI: `--frozen-lock` fails (`SDK_DRIFT`) instead of changing the lock, and fails while the lock",
2160
+ "carries an entry no exported object matches \u2014 a plain `export` only warns and writes that",
2161
+ "entry down, after which nothing tells a rename from a deletion: resolve it with",
2162
+ "`lock rename` or `lock prune`, or accept them with `--allow-lock-orphans` when they are the",
2163
+ "not-yet-ported half of an adopted workspace.",
2164
+ "`export --check` implies `--frozen-lock` and writes nothing \u2014 no bundle, no lock \u2014 so it",
2165
+ "reads no `xano/.env` or `.secrets.json` and refuses the flags that supply them; this is",
2166
+ "what `npm run xano:check` runs, so CI needs no secrets. With no lock at all, either flag",
2167
+ "fails only when the source has identities to record (an empty workspace passes).",
2168
+ ""
2169
+ ];
2170
+ const stmtCalls = [
2171
+ "Auth & calls:",
2172
+ "",
2173
+ "- `s.security.create_auth_token({ table, id, extras?, expiration?, as? })` \u2014 `extras` defaults to `{}`, `expiration` (seconds, a `Value`: `c.int(3600)`) to 86400 (`c.int(0)` = never).",
2174
+ '- **Marketplace first:** `@xano-sdk/auth` registers a complete auth surface \u2014 signup/login/me endpoints plus their `user`/`account`/`event_log` tables \u2014 via `registerAuth(ws, { canonical: "authn" })`. Authentication only, not authorization. Inspect with `xanosdk marketplace details @xano-sdk/auth --prompt`.',
2175
+ "- **The auth loop end to end** \u2014 what that module implements; author it directly for a custom flow. An auth TABLE (`table({ auth: true })`) backs identity; a PUBLIC login query verifies and mints a token; every protected query names that table as `auth:` and reads the caller with `auth(...)`.",
2176
+ ' - Signup: `s.db.add({ table: users, row: { email: inp("email"), password: inp("password") } })` \u2014 the `f.password()` column hashes on write; take the submission as `input.text()` on signup AND login (see Gotchas on double-hashing).',
2177
+ ' - Login: `s.db.get({ table: users, fieldName: "email", fieldValue: inp("email"), output: ["id", "password"], as: "u" })` \u2014 the `output` naming `password` is REQUIRED (the column is `access: "internal"` and absent from the row otherwise) \u2192 `s.precondition({ expr: expr(ref("u", { safe: true }), "!=", c.null()), error: c.text("No such user."), error_type: "notfound" })` \u2192 `s.security.check_password({ text_password: inp("password"), hash_password: ref("u.password"), as: "ok" })` \u2192 precondition on `ok` \u2192 `s.security.create_auth_token({ table: users, id: ref("u.id"), as: "token" })` \u2192 `response: { token: ref("token") }`.',
2178
+ ' - Client: send it as an `Authorization: Bearer <token>` header. A `query({ auth: users })` refuses a request without a valid one before its stack runs; inside the stack `auth("id")` is the caller\'s row id (`response: { id: auth("id") }` is the whole `me` endpoint).',
2179
+ '- **Authorization, on top of that.** `guard.role(users, "admin")` and `guard.owner("note", "user_id")` each return a fixed-arity TUPLE of statements \u2014 spread them (`stack: [...guard.role(users, "admin"), \u2026]`); a helper returning `Statement[]` widens the stack and kills `InferResponse`.',
2180
+ ' - `guard.role(table, role | roles[], { field?, as?, errorType?, message?, missingErrorType?, missingMessage? })` \u2014 fetches the caller\'s own row by `auth("id")` into `__actor`, asserts it EXISTS (401), then asserts its `role` is in the set (403). Roles type from the enum column; an unknown role or `field` throws. The existence step matters: a role check on a null actor row is not reliably false.',
2181
+ ' - `guard.owner(rowVar, ownerField = "user_id", { \u2026 })` \u2014 asserts the row bound to `rowVar` is non-null (404), THEN compares its owner column to `auth("id")` (also 404). Drilling before the existence check is a 500, not a denial: `db.get` binds `null` on a miss and `ref("note.user_id")` against it raises `Unable to locate var`. Takes the var NAME and the column separately for exactly that reason; a dotted path is refused.',
2182
+ ' - `guard.found(rowVar, { errorType?, message? })` \u2014 ONE statement: asserts the row bound to `rowVar` is non-null (404, `"Not found."`), and NARROWS it \u2014 after it, `InferResponse` drops the `| null` from `rowVar`. `guard.owner`\'s existence step is this statement.',
2183
+ " - `guard.require(condition, { errorType?, message? })` for anything else \u2014 same shape, and it defaults to 403 where a bare `s.precondition` defaults to `standard`, i.e. **500**.",
2184
+ "- `s.security.create_uuid({ as? })` \u2014 bind a fresh UUID (v4). Takes nothing else. There is NO `create_guid` (internal, unpushable).",
2185
+ '- **Receiving a webhook** \u2014 no `auth`, no declared `input` (the signature covers the raw bytes): `s.util.get_raw_input({ encoding: "none", as: "raw" })` \u2192 `s.set_var("mac", withFilters(ref("raw"), fl.hmac_sha256(env("WH_SECRET"))))` \u2192 `s.set_var("sig", withFilters(sys.httpHeaders(), fl.get("X-Signature")))` (Title-Cased) \u2192 an `s.precondition` (`error_type: "unauthorized"`) comparing the two TIMING-SAFELY by HMACing both again \u2014 `expr(withFilters(ref("sig"), fl.hmac_sha256(env("WH_SECRET"))), "=", withFilters(ref("mac"), fl.hmac_sha256(env("WH_SECRET"))))` \u2014 BEFORE `fl.json_decode()` reads `raw`. Claim the event id with `s.redis.set({ \u2026, create_only: c.bool(true) })` and answer 200 to a replay.',
2186
+ "- `s.function.run({ fn, input?, as?, runtime? })` / `s.function.call({ fn, input?, as? })` \u2014 run another function; `input` is keyed by the target's input names (an undeclared one is a type and export error; generic over `fn`, pass `Record<string, Value>`; `2` binds an int \u2014 `c.decimal(2)` for a decimal). \u26A0 `s.function.run` binds an input it OMITS from the CALLING def's same-named declared input \u2014 an endpoint declaring `discount` hands `?discount=100` through, or `0` when the client sends none. An endpoint/tool/prompt declaring NO inputs binds it from the raw request. `ignored()` always takes the target's default. Pass inputs explicitly (`statement.omitted-input` warns).",
2187
+ ' - `runtime?` runs it in the BACKGROUND: `{ mode: "async-shared" }` or `{ mode: "async-dedicated", cpu?, memory?, timeout?, maxRetry? }` (resources read at dedicated only). An async call DOES NOT return the result \u2014 it dispatches and continues, and `as` binds the job id (a UUID `string`); collect with `s.await({ ids, timeout, as })`, binding `{ "<jobId>": result }`; a too-short `timeout` or unknown id is a 500 `Async timeout`, no partial result. Omit for a normal call; same on `s.ai.agent.run`.',
2188
+ "- `s.api.call({ api, input?, headers?, auth?, as? })` \u2014 invoke an endpoint. WORKFLOW-TEST ONLY (see Gotchas in `llms.txt`).",
2189
+ " - `api` takes the `query()` def HANDLE (or a `{ name, guid }` pair) \u2014 a bare name is refused, because a query's identity is composed from its api group, verb, and name.",
2190
+ " - `headers` REPLACES the request headers the callee sees and takes the same shapes as `s.api.request`'s, or one `Value`.",
2191
+ " - `auth` is `{ token, ignoreExpiration? }`: `token` must be a BARE STRING \u2014 a tagged `Value` deploys clean and then fails the run with `Param: token - Text filter requires an integer, float, string or boolean value`, since the engine stores that slot as plain text and never evaluates it.",
2192
+ " - Neither slot authenticates the call today; see `llms/tests.md` for what a workflow-test run actually sees.",
2193
+ "- `s.api.request({ url, method?, params?, headers?, timeout?, follow_location?, verify_host?, verify_peer?, ca_certificate?, certificate?, certificate_pass?, private_key?, private_key_pass?, description?, output?, as? })` \u2014 external HTTP request (`mvp:api_request`). Ergonomic types, each also accepting a dynamic `Value`:",
2194
+ " - `method` suggests the 7 verbs (GET/POST/PUT/DELETE/HEAD/OPTIONS/PATCH).",
2195
+ ' - `params` a plain JSON object **or** a FLAT record whose values are tagged `Value`s (`{ count: ref("count") }`, each lifted via a `set` filter \u2014 the same record-of-values shape `response: { key: value }` takes); a tagged value NESTED inside an object or array THROWS at encode, so wrap a structured body in `obj({...})`, which encodes any depth as one `const:expr2` (\u2192 query string for GET/HEAD/OPTIONS, body otherwise).',
2196
+ ' - `headers` a `{ "Name": value }` record whose values may be tagged (`{ "x-api-key": env("KEY") }`, each pair joined to a `"Name: value"` line) **or** a `string[]` of full header lines \u2014 prefer a header over a `?key=` query param for a credential \u2014 a URL reaches access logs and proxies.',
2197
+ " - \u26A0 Neither spelling is envelope-safe: the `as` envelope's `request` half mirrors `url`, `params` AND `headers`, so never return it raw from a credentialed request \u2014 read `response.result`.",
2198
+ " - A NAME outside the header-token charset is refused, and a LITERAL value (string or `c.text`) carrying CR/LF/NUL is refused; a value may hold `:` and spaces (`Bearer a: b` is a valid value). A TAGGED value cannot be checked at build time, so strip CRLF from caller-controlled input before this slot. Literal pairs are emitted before computed ones, whatever the key order.",
2199
+ ' - No `User-Agent` is sent unless `headers` carries one. An API requiring one answers **403** as ordinary `response.result` (no throw; every mapped field reads `null`). Send one (`headers: { "User-Agent": c.text("my-app") }`) and gate on `ref("<var>.response.status")` = 200 with `s.precondition`.',
2200
+ " - A transport failure (timeout, DNS) does not throw \u2014 `s.try_catch` never catches it: `response.status` is `0`, with `response.error` set. Retry on `status == 0 || status >= 500`; `status < 500` passes a 0.",
2201
+ " - `timeout` a `number` in seconds (1\u201386400), and `follow_location`/`verify_host`/`verify_peer` booleans.",
2202
+ " - `description` and `output` filters ride the envelope. Cert pairings (certificate\u2194private_key, ca_certificate\u2192verify_peer) are checked at build time when provable.",
2203
+ " - The `as` result is typed as the `{request, response}` envelope (`response.status: number`, `response.result: unknown`), so `InferResponse` resolves a `ref` to it. Same typed result on `webflow.request` and `microservice.request`.",
2204
+ "- `s.stream.from_request({ url, method?, \u2026tls, as? })` \u2014 streaming external HTTP request (`mvp:streaming_api_request`); same typed field surface as `s.api.request` (no description/output envelope). `url` is REQUIRED.",
2205
+ '- `s.util.send_email({ to, subject, message, from?, cc?, bcc?, reply_to?, service_provider?, api_key?, scheduled_at?, as? })` \u2014 send email from the stack. `service_provider` is `"xano"` (the built-in mailer \u2014 needs NO `api_key` and no configuration, and does not require a verified sender) or `"resend"` (pass the key as `api_key: env("RESEND_API_KEY")`). Prefer this over hand-rolling `s.api.request` against a mail provider.',
2206
+ "- `s.webflow.request({ path, method?, \u2026tls, as? })` \u2014 Webflow API request (`mvp:connect_webflow_api_request`); like `s.api.request` but addressed by `path` (host is engine-supplied), and `path` is REQUIRED \u2014 the engine rejects an empty one. No `headers`: the engine builds its own from the workspace's Webflow connection and ignores an authored value.",
2207
+ "- `s.task.call` / `s.tool.call` / `s.trigger.call` / `s.middleware.call` / `s.addon.call` \u2014 same `{ <target>, input?, as? }` shape against the named kind. `s.task.call` and `s.trigger.call` are WORKFLOW-TEST ONLY (see Gotchas in `llms.txt`); `s.tool.call`, `s.middleware.call` and `s.addon.call` run from any stack.",
2208
+ "- `s.action.call({ actionId, input?, registry?, as? })` / `s.action.package.call({ traceId, versionId, slug, input?, registry?, as? })` \u2014 invoke an installed action. Ids are SUPPLIED, never derived from a name: an action is installed onto the instance, so its identity is assigned at install and differs per instance \u2014 read them off a call in a pulled workspace. The package form needs all three parts; they are one composite and two of them address nothing. `registry` is the action's own settings, `input` the per-call arguments.",
2209
+ "- `s.cloud.job({ image, command, args?, secret?, template?, await?, as? })` \u2014 launch a containerized job. `image`/`command` are REQUIRED; omitted \u2192 `Missing param`. \u26A0 `await` is SECONDS (default 60), not a boolean.",
2210
+ "- `s.cloud.job.status({ id, as? })` \xB7 `s.cloud.job.await({ ids, timeout, as? })` \u2014 poll one job, or block on several (`ids` a list `Value`, `timeout` in seconds; both required).",
2211
+ "",
2212
+ "Microservices (the `microservice()` def and the statement that calls it):",
2213
+ "",
2214
+ "- `microservice({ name, kind, \u2026 })` \u2014 two mutually exclusive shapes via `kind`: `builtin` declares containers (image/ports/resources/env/command/args) plus optional `ingresses`, and `helm` points at a chart and its `values` \u2014 passing both throws.",
2215
+ " - EARLY SURFACE, expected to change \u2014 every export of a workspace declaring one prints a notice saying so.",
2216
+ " - `configs`/`volumes` are typed and `@deprecated` but NOT deployable: the engine rejects an import carrying either, so `export()` fails the build rather than letting the deploy fatal.",
2217
+ " - Put a value the workload reads in a container's `env`, and storage in a container's own `volumes` (`emptyDir`/`persistent`/`config`).",
2218
+ " - Container names are free-form \u2014 they need not match the microservice name, which is what a stack addresses.",
2219
+ "- SECRETS RIDE ALONG \u2014 `chart.values` and `registryAuth.dockerconfigjson` are carried into the bundle, and into a pulled tree, verbatim. `process.env.X` in the def is NOT a way out: it resolves at EXPORT and writes the literal in.",
2220
+ ' - Reference a `workspaceConfig({ env })` var instead and only the NAME travels \u2014 the engine substitutes at DEPLOY. Two spellings: `env: [{ name: "API_KEY", fromEnv: "STRIPE_SECRET" }]` on a builtin container (`value`/`fromEnv` exclusive; both \u2192 build error), or `${env.NAME}` inside a helm `chart.values` (a VALUE at any depth, never a key). A name the workspace does not define FAILS the deploy rather than resolving blank.',
2221
+ " - `registryAuth.dockerconfigjson` has NO such form: either leave it unset and pull a public image, or treat the bundle and any tree pulled from it as secret material \u2014 out of git, or rotate after. Export prints a notice per microservice carrying a literal; `--strict` does not promote it.",
2222
+ "- `s.microservice.request({ host, path, port?, method?, params?, headers?, timeout?, follow_location?, as? })` \u2014 in-cluster microservice call (`mvp:microservice_request`); no TLS fields.",
2223
+ " - ONLY `host`+`path` required; the rest default to the engine's values (`GET`/`{}`/`[]`/`10`/`true`), always emitted.",
2224
+ " - Pass the `microservice()` DEF as `host` \u2014 it binds by NAME (how the engine resolves it), so a rename fixes every call site and the port is checked before deploy.",
2225
+ ' - `port?` folds into `host` as `"name:port"`: a def exposing ONE `servicePort` resolves automatically, SEVERAL requires it.',
2226
+ ' - A raw `"name:port"` string works, unvalidated, and is the only way to reach an instance-level microservice.',
2227
+ ' - `tenantDeploy: "manual"` on the def imports the row without starting the workload.',
2228
+ "- `s.workflow_test.call({ workflowTest, datasource?, as? })` \u2014 run another workflow test from inside one, which is the only place it runs (see Gotchas in `llms.txt`). The odd one out: NO `input` (a workflow test takes none), and it carries `datasource?` instead \u2014 same clone caveat as the kind's own field.",
2229
+ ""
2230
+ ];
2231
+ const groups = /* @__PURE__ */ new Map();
2232
+ for (const s of m.statements) {
2233
+ if (s.legacy) continue;
2234
+ const ns = s.sPath.includes(".") ? s.sPath.slice(0, s.sPath.indexOf(".")) : "(top-level)";
2235
+ (groups.get(ns) ?? groups.set(ns, []).get(ns)).push(s);
2236
+ }
2237
+ const stmtCatalog = [];
2238
+ for (const ns of [...groups.keys()].sort()) {
2239
+ stmtCatalog.push(`### ${ns}`, "");
2240
+ const note = NAMESPACE_NOTES[ns];
2241
+ if (note) stmtCatalog.push(note, "");
2242
+ for (const s of groups.get(ns)) {
2243
+ const call = `s.${s.sPath}`;
2244
+ const flags = [s.declarative ? null : "special", s.registered ? null : "unregistered", s.output ? "output" : null].filter(Boolean).join(", ");
2245
+ const flagSuffix = flags ? ` [${flags}]` : "";
2246
+ const resultSuffix = s.result ? ` \u2192 ${s.result.name}: ${s.result.type}${s.result.note ? ` (${s.result.note})` : ""}` : "";
2247
+ if (s.fields) {
2248
+ const args = s.fields.map((f) => fieldLine(f, s.sPath)).join("; ");
2249
+ stmtCatalog.push(`- \`${call}({ ${args} })\`${flagSuffix}${resultSuffix}`);
2250
+ } else {
2251
+ stmtCatalog.push(`- \`${call}\`${flagSuffix}${resultSuffix}`);
2252
+ }
2253
+ }
2254
+ stmtCatalog.push("");
2255
+ }
2256
+ const legacy = [];
2257
+ const legacyValues = m.values.constructors.filter((v) => v.legacy);
2258
+ const legacyStatements = m.statements.filter((s) => s.legacy);
2259
+ const legacyFactories = m.objectKinds.flatMap((k) => (k.subKinds ?? []).filter((sub) => sub.legacy));
2260
+ if (legacyValues.length + legacyStatements.length + legacyFactories.length + SUPERSEDED_STATEMENTS.size + DECODE_ONLY_STATEMENTS.size > 0) {
2261
+ legacy.push(
2262
+ // The heading the topic file drops in favor of its own title; kept so the
2263
+ // block reads as a section wherever it is rendered from.
2264
+ "## Legacy",
2265
+ "",
2266
+ "Older paradigms this SDK still supports and still emits when it decodes an existing",
2267
+ "workspace. **Do not author these.** They are listed by name only so you recognize them",
2268
+ 'in pulled code rather than "fixing" them; each line names what to use instead.',
2269
+ "",
2270
+ "Names overlap across the split deliberately \u2014 the engine reused words like",
2271
+ '"realtime" and "channel" for both generations. A name matching is NOT evidence that',
2272
+ "two things are the same object; check which list it came from.",
2273
+ ""
2274
+ );
2275
+ for (const v of legacyValues) legacy.push(`- \`${v.name}\` \u2014 ${v.description}`);
2276
+ for (const f of legacyFactories) legacy.push(`- \`${f.authorFactory}()\` \u2014 ${f.description}`);
2277
+ for (const s of legacyStatements) {
2278
+ legacy.push(`- \`s.${s.sPath}\` \u2014 ${LEGACY_SURFACES[s.surface]}`);
2279
+ }
2280
+ const retired = [...SUPERSEDED_STATEMENTS.entries()];
2281
+ if (retired.length > 0) {
2282
+ legacy.push("");
2283
+ legacy.push(
2284
+ "Retired statement VERSIONS \u2014 no `s.` surface exists. Pulled code shows them as",
2285
+ '`raw({ name: "\u2026" })` and they keep running as stored, so leave them; author the',
2286
+ "replacement only for NEW code. Never swap one for the other \u2014 each version broke the last.",
2287
+ ""
2288
+ );
2289
+ for (const [stored, successor] of retired) {
2290
+ legacy.push(successor ? `- \`${stored}\` \u2192 \`${successor}\`` : `- \`${stored}\` \u2014 retired, no replacement`);
2291
+ }
2292
+ }
2293
+ const decodeOnly = [...DECODE_ONLY_STATEMENTS.entries()];
2294
+ if (decodeOnly.length > 0) {
2295
+ legacy.push("");
2296
+ legacy.push(
2297
+ "Statements the engine writes but will NOT import back \u2014 no `s.` surface exists, and",
2298
+ "unlike the retired versions above these must be FIXED, not left alone. Pulled code shows",
2299
+ 'them as `raw({ name: "\u2026" })`; `export()` refuses any bundle that still contains one.',
2300
+ ""
2301
+ );
2302
+ for (const [stored, reason] of decodeOnly) legacy.push(`- \`${stored}\` \u2014 ${reason}.`);
2303
+ }
2304
+ legacy.push("");
2305
+ }
2306
+ legacy.push(
2307
+ "## Reading a pulled tree",
2308
+ "",
2309
+ "`codegen` emits objects as FACTORY calls (`table({...})`, `query({...})`, \u2026) \u2014 the form",
2310
+ "you author by hand \u2014 so inference survives the round trip.",
2311
+ 'Three shapes read differently and must not be "fixed": a trigger its factory cannot',
2312
+ "reproduce (its target outside the pulled tree, or `meta` it cannot write) stays",
2313
+ "`{...} satisfies TriggerDef`; a",
2314
+ "statement the catalog cannot model round-trips verbatim through `raw()`",
2315
+ "(`@xano/sdk/codegen`); and an object ALREADY EMPTY upstream decodes to a def with",
2316
+ "no `stack`, reported as `empty-source` \u2014 faithful, not a decode failure. Workspace env",
2317
+ 'var VALUES go to `xano/.env` (owner-only, gitignored); `xano/workspace.ts` declares each as `""`.',
2318
+ ""
2319
+ );
2320
+ return {
2321
+ router: lines,
2322
+ lock,
2323
+ legacy,
2324
+ objectKinds,
2325
+ kindsCore,
2326
+ client,
2327
+ toolchain,
2328
+ errors,
2329
+ kindsAgentMcp,
2330
+ kindsKnowledge,
2331
+ kindsRealtime,
2332
+ triggers,
2333
+ stmtData,
2334
+ stmtRuntime,
2335
+ stmtCalls,
2336
+ stmtCatalog,
2337
+ values,
2338
+ fields,
2339
+ filters,
2340
+ lambda,
2341
+ tests: renderTestsSection()
2342
+ };
2343
+ }
2344
+ var LLMS_TXT = "llms.txt";
2345
+ var LLMS_FULL_TXT = "llms-full.txt";
2346
+ function renderTopic(t) {
2347
+ const body = [...t.body];
2348
+ if (t.dropLeadingHeading) {
2349
+ body.shift();
2350
+ while (body.length > 0 && body[0] === "") body.shift();
2351
+ }
2352
+ while (body.length > 0 && body[body.length - 1] === "") body.pop();
2353
+ return [`# ${t.title}`, "", `> ${t.readWhen}`, "", ...body, ""].join("\n");
2354
+ }
2355
+ function renderDocs(m) {
2356
+ const s = renderSections(m);
2357
+ const topics = [
2358
+ {
2359
+ path: "llms/object-kinds.md",
2360
+ title: "Object kinds",
2361
+ readWhen: "Read when deciding WHAT to build \u2014 every authorable primitive, what each is, and which factory + register method builds it. Also when `register*` will not typecheck an array built by `.flatMap()`/`.concat()`.",
2362
+ body: s.objectKinds,
2363
+ dropLeadingHeading: true
2364
+ },
2365
+ {
2366
+ path: "llms/kinds-core.md",
2367
+ title: "Core def shapes",
2368
+ readWhen: "Read when authoring a function, query, api group, task, workflow test, middleware, tool, or addon \u2014 and for the `response` and `expr` shapes every one of them uses.",
2369
+ body: s.kindsCore,
2370
+ dropLeadingHeading: true
2371
+ },
2372
+ {
2373
+ path: "llms/client.md",
2374
+ title: "Consuming defs from a client",
2375
+ readWhen: "Read when a frontend imports a def (`getPath()`/`InferResponse`) or calls the deployed backend \u2014 what the import costs, `window.XANO_HOST`, local dev, file URLs, and a Node spot-check.",
2376
+ body: s.client
2377
+ },
2378
+ {
2379
+ path: "llms/toolchain.md",
2380
+ title: "Toolchain modules",
2381
+ readWhen: 'Read when `package.json` has a `"xanosdk"` block or you run `xanosdk marketplace`.',
2382
+ body: s.toolchain
2383
+ },
2384
+ {
2385
+ path: "llms/errors.md",
2386
+ title: "Error index",
2387
+ readWhen: "Read when a request, test, build, or pull fails with a message you did not write \u2014 the string, its cause, and where the fix is.",
2388
+ body: s.errors
2389
+ },
2390
+ {
2391
+ path: "llms/tests.md",
2392
+ title: "Saved unit tests, assertions, and mocks",
2393
+ readWhen: "Read when authoring a `workflowTest()` stack, when a query/function/middleware carries `tests`, when a statement needs a `mock`, or when running a deployed environment's tests.",
2394
+ body: s.tests,
2395
+ dropLeadingHeading: true
2396
+ },
2397
+ {
2398
+ path: "llms/kinds-agent-mcp.md",
2399
+ title: "Agent and MCP def shapes",
2400
+ readWhen: "Read when defining an `agent()`, `mcpServer()`, `prompt()` or `resource()`.",
2401
+ body: s.kindsAgentMcp
2402
+ },
2403
+ {
2404
+ path: "llms/kinds-knowledge.md",
2405
+ title: "Knowledge def shape",
2406
+ readWhen: "Read when the workspace defines a `knowledge()` item for its AI agents.",
2407
+ body: s.kindsKnowledge
2408
+ },
2409
+ {
2410
+ path: "llms/kinds-realtime.md",
2411
+ title: "Realtime def shapes",
2412
+ readWhen: "Read when the workspace defines a `realtimeServer()`, `realtimeChannel()`, or `realtimeMessage()` handler.",
2413
+ body: s.kindsRealtime
2414
+ },
2415
+ {
2416
+ path: "llms/triggers.md",
2417
+ title: "Triggers",
2418
+ readWhen: "Read when authoring any trigger. A trigger's `stack` is a callback rather than the plain array every other kind takes, so the shape does not carry over.",
2419
+ body: s.triggers,
2420
+ dropLeadingHeading: true
2421
+ },
2422
+ {
2423
+ path: "llms/statements-data.md",
2424
+ title: "Array and database statements",
2425
+ readWhen: "Read when the stack reads or writes rows (`s.db.*`), or transforms an array in place (`s.array.map`, `s.array.union`).",
2426
+ body: s.stmtData
2427
+ },
2428
+ {
2429
+ path: "llms/statements-runtime.md",
2430
+ title: "Statement runtime behavior",
2431
+ readWhen: "Read when you need to know what a statement's `as:` output holds, or why a bound variable is not the shape expected.",
2432
+ body: s.stmtRuntime
2433
+ },
2434
+ {
2435
+ path: "llms/statements-calls.md",
2436
+ title: "Auth, cross-object calls, and microservices",
2437
+ readWhen: "Read when the stack authenticates or verifies a webhook (recipes here), calls another object (`s.function.run`, `s.api.request`, `s.tool.call`), sends email (`s.util.send_email`), or reaches a microservice.",
2438
+ body: s.stmtCalls
2439
+ },
2440
+ {
2441
+ path: "llms/values.md",
2442
+ title: "Value catalog",
2443
+ readWhen: "Read when you need a literal, a reference, or a tag you have not used before \u2014 `c.*`, `ref`, `inp`, `auth`, `col`, and what each one encodes to.",
2444
+ body: s.values,
2445
+ dropLeadingHeading: true
2446
+ },
2447
+ {
2448
+ path: "llms/fields.md",
2449
+ title: "Column and input types",
2450
+ readWhen: "Read when declaring a table column (`f.*`) or a function/query input (`input.*`) \u2014 a type's options and methods, and the `s.precondition` error/status contract. Also `table({ seed })` rows and `use_xdo`.",
2451
+ body: s.fields,
2452
+ dropLeadingHeading: true
2453
+ },
2454
+ {
2455
+ path: "llms/filters.md",
2456
+ title: "Filter catalog",
2457
+ readWhen: "Read when piping a value through `fl.*` \u2014 the full catalog with each filter's argument types.",
2458
+ body: s.filters,
2459
+ dropLeadingHeading: true
2460
+ },
2461
+ {
2462
+ path: "llms/lambda.md",
2463
+ title: "Lambda bodies (JavaScript)",
2464
+ readWhen: "Read when writing a JavaScript body, or weighing whether to reach for one at all \u2014 `lam.fn`, `fl.lambda`, `fl.reduce`, `s.lambda`; each surface binds a different set of identifiers.",
2465
+ body: s.lambda,
2466
+ dropLeadingHeading: true
2467
+ },
2468
+ {
2469
+ path: "llms/lock.md",
2470
+ title: "Lock file",
2471
+ readWhen: "Read when a `xano.lock` exists or should \u2014 renaming/pruning/importing identities, seeding the lock programmatically, or asking which commands write it.",
2472
+ body: s.lock,
2473
+ dropLeadingHeading: true
2474
+ },
2475
+ {
2476
+ path: "llms/legacy.md",
2477
+ title: "Legacy paradigms and retired statements",
2478
+ readWhen: 'Read when the code was PULLED from an existing Xano instance \u2014 how a codegen\'d tree reads, the shapes in it that must not be "fixed", and any `raw({ name: "mvp:\u2026" })`, `realtimeTrigger()`, or name the catalogs do not list.',
2479
+ body: s.legacy,
2480
+ dropLeadingHeading: true
2481
+ },
2482
+ {
2483
+ path: "llms/statements-catalog.md",
2484
+ title: "Statement catalog",
2485
+ readWhen: "Read for the field signature of a specific statement \u2014 every surface, grouped by `s.*` namespace.",
2486
+ body: s.stmtCatalog
2487
+ }
2488
+ ];
2489
+ const nav = [
2490
+ "## Topic files",
2491
+ "",
2492
+ "Paths are relative to this file (`node_modules/@xano/sdk/` once installed), so a",
2493
+ "plain file read resolves them at the version you have.",
2494
+ `\`${LLMS_FULL_TXT}\` is everything concatenated \u2014 one fetch for a reader that cannot open files.`,
2495
+ "",
2496
+ ...topics.map((t) => `- [${t.title}](${t.path}): ${t.readWhen}`),
2497
+ ""
2498
+ ];
2499
+ const slot = s.router.indexOf(NAVIGATION_SLOT);
2500
+ if (slot === -1) throw new Error(`the router lost its ${NAVIGATION_SLOT} placeholder`);
2501
+ const router = [...s.router.slice(0, slot), ...nav, ...s.router.slice(slot + 1)];
2502
+ const docs = /* @__PURE__ */ new Map([[LLMS_TXT, router.join("\n")]]);
2503
+ for (const t of topics) docs.set(t.path, renderTopic(t));
2504
+ docs.set(
2505
+ LLMS_FULL_TXT,
2506
+ [docs.get(LLMS_TXT) ?? "", ...topics.map((t) => docs.get(t.path) ?? "")].join("\n")
2507
+ );
2508
+ return docs;
2509
+ }
2510
+ function renderLlmsTxt(m) {
2511
+ const txt = renderDocs(m).get(LLMS_TXT);
2512
+ if (txt === void 0) throw new Error(`renderDocs emitted no ${LLMS_TXT}`);
2513
+ return txt;
2514
+ }
2515
+ export {
2516
+ CAPABILITIES,
2517
+ FIELD_METHODS,
2518
+ FILTER_NAMES,
2519
+ GENERATED_SPECS,
2520
+ GENERATED_STATEMENT_NAMES,
2521
+ IMPLEMENTED_STATEMENTS,
2522
+ KNOWLEDGE_DIR,
2523
+ KNOWLEDGE_FILE,
2524
+ LLMS_TXT,
2525
+ LOCK_PAYLOAD_KEYS,
2526
+ LOCK_VERSION,
2527
+ PAYLOAD_ARRAY_KEYS,
2528
+ QUERY_EXPRESSION_FILTERS,
2529
+ REFERENCEABLE_KINDS,
2530
+ STATEMENT_CAPABILITIES,
2531
+ STATEMENT_SURFACES,
2532
+ TOTAL_OBJECT_KINDS,
2533
+ TOTAL_STATEMENTS,
2534
+ VECTOR_FILTERS,
2535
+ WORKSPACE_KEY,
2536
+ addonKind,
2537
+ adoptFromBundle,
2538
+ agentKind,
2539
+ apiGroupKind,
2540
+ buildBundle,
2541
+ buildManifest,
2542
+ buildMiddlewareBlock,
2543
+ buildWorkspaceHistory,
2544
+ bundleCapabilities,
2545
+ calcSignatureJson,
2546
+ channelPathParams,
2547
+ createLockContext,
2548
+ declaredServicePorts,
2549
+ deriveGuid,
2550
+ emptyLock,
2551
+ encodeAddon,
2552
+ encodeAgent,
2553
+ encodeApiGroup,
2554
+ encodeColumn,
2555
+ encodeContainerHistory,
2556
+ encodeFromSpec,
2557
+ encodeFunction,
2558
+ encodeHistory,
2559
+ encodeIndex,
2560
+ encodeKnowledge,
2561
+ encodeMcpServer,
2562
+ encodeMicroservice,
2563
+ encodeMiddleware,
2564
+ encodeMiddlewareEntry,
2565
+ encodeMiddlewareList,
2566
+ encodeObject,
2567
+ encodeQuery,
2568
+ encodeRealtimeChannel,
2569
+ encodeRealtimeMessage,
2570
+ encodeRealtimeServer,
2571
+ encodeResponse,
2572
+ encodeSchedule,
2573
+ encodeStatement,
2574
+ encodeTable,
2575
+ encodeTask,
2576
+ encodeTool,
2577
+ encodeToolRefs,
2578
+ encodeToolsetBase,
2579
+ encodeTrigger,
2580
+ encodeView,
2581
+ encodeWorkflowTest,
2582
+ encodeWorkspaceConfig,
2583
+ functionKind,
2584
+ generated as generatedStatements,
2585
+ getKind,
2586
+ getLockedCanonical,
2587
+ getLockedGuid,
2588
+ getStatementFactory,
2589
+ isKnowledgeDirSource,
2590
+ isKnowledgeFileSource,
2591
+ isLockSeeded,
2592
+ isQueryExpressionFilter,
2593
+ isRegisteredKind,
2594
+ isRegisteredStatement,
2595
+ knowledgeKind,
2596
+ lockKey,
2597
+ mcpServerKind,
2598
+ mergeObserved,
2599
+ microserviceKind,
2600
+ middlewareKind,
2601
+ mintCanonical,
2602
+ parseLock,
2603
+ phpJsonEncode,
2604
+ queryKind,
2605
+ realtimeChannelGuid,
2606
+ realtimeChannelKind,
2607
+ realtimeMessageGuid,
2608
+ realtimeMessageKind,
2609
+ realtimeServerKind,
2610
+ recordObserved,
2611
+ registerKind,
2612
+ registerSpec,
2613
+ registeredKinds,
2614
+ renameLockEntry,
2615
+ renderDocs,
2616
+ renderLlmsTxt,
2617
+ resolveAuthRef,
2618
+ resolvePayloadKey,
2619
+ resolveRealtimeServerCanonical,
2620
+ resolveRef,
2621
+ resolveToolsetCanonical,
2622
+ sPathOf,
2623
+ serializeBundle,
2624
+ serializeLock,
2625
+ tableKind,
2626
+ taskKind,
2627
+ toNestedFields,
2628
+ toolKind,
2629
+ triggerKind,
2630
+ validateLockModel,
2631
+ workflowTestKind,
2632
+ workspaceKind
2633
+ };
2634
+ //# sourceMappingURL=internal.js.map