@xanots/sdk 0.0.11 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +16 -9
  3. package/dist/.build-fingerprint +1 -1
  4. package/dist/{agent-file-refresh-QNKN5RYD.js → agent-file-refresh-GQWAAOBV.js} +4 -4
  5. package/dist/bin.js +11 -11
  6. package/dist/{branch-commands-2BLOC2GR.js → branch-commands-5KMPAAZV.js} +7 -7
  7. package/dist/bundle.d.ts +2 -2
  8. package/dist/bundle.js +2 -2
  9. package/dist/{capture-4WVJY4DQ.js → capture-YLUVAITI.js} +2 -2
  10. package/dist/{chunk-22TKBSDV.js → chunk-2AY3PKF4.js} +2 -2
  11. package/dist/chunk-2VTJSI6X.js +192 -0
  12. package/dist/{chunk-5XZ744TS.js → chunk-2ZCUO2UG.js} +1 -1
  13. package/dist/{chunk-XEOX6AM7.js → chunk-3LQGF2WS.js} +2 -2
  14. package/dist/{chunk-WP4OZZV4.js → chunk-3VFCKHOB.js} +2 -2
  15. package/dist/{chunk-XHEXOES3.js → chunk-5YDINUAP.js} +1 -1
  16. package/dist/{chunk-LU7TRWMC.js → chunk-6VNRMKFJ.js} +2 -2
  17. package/dist/{chunk-CTD5ZCV6.js → chunk-7WRJPKGK.js} +2 -2
  18. package/dist/{chunk-3INK4Y4E.js → chunk-AE3PDSDS.js} +1 -1
  19. package/dist/{chunk-DBFU47BJ.js → chunk-AOFFKSJC.js} +2 -2
  20. package/dist/{chunk-TCFIPDB3.js → chunk-AVGDL6RB.js} +1 -1
  21. package/dist/{chunk-UOZMSF4C.js → chunk-BYQHCCYU.js} +5 -5
  22. package/dist/{chunk-BC2C5GVI.js → chunk-DCMANKMX.js} +1 -1
  23. package/dist/{chunk-WHOJWOSV.js → chunk-EETVJZAZ.js} +1 -1
  24. package/dist/{chunk-VNQM3V2C.js → chunk-EXENFOWE.js} +2 -2
  25. package/dist/{chunk-AIZKXUNP.js → chunk-IMLYGQK6.js} +2 -2
  26. package/dist/{chunk-OWGCOGKK.js → chunk-MEFMTICH.js} +83 -8
  27. package/dist/{chunk-QK7ZQJLP.js → chunk-N74KDCBD.js} +39 -25
  28. package/dist/{chunk-3IGNIP6R.js → chunk-NDP7OUPS.js} +1 -1
  29. package/dist/{chunk-4Q7ZOHH7.js → chunk-OONT4ZL4.js} +3 -3
  30. package/dist/{chunk-EQW3YT5U.js → chunk-P6PVBQL6.js} +2 -2
  31. package/dist/{chunk-QYMAZRAU.js → chunk-PJNWOZMT.js} +5 -5
  32. package/dist/{chunk-BSK7ELHU.js → chunk-PR7OXHGZ.js} +1 -1
  33. package/dist/{chunk-RCT7UX7B.js → chunk-RLI6XD4O.js} +32 -27
  34. package/dist/{chunk-ZQ2PKR6R.js → chunk-RQ3FXV4K.js} +2 -2
  35. package/dist/{chunk-Q77KNEUL.js → chunk-RQNMTDXD.js} +1703 -2
  36. package/dist/{chunk-4IF54NU5.js → chunk-S3DOJOW4.js} +41 -21
  37. package/dist/{chunk-HYBN4H3F.js → chunk-SS2V2QOG.js} +27 -20
  38. package/dist/{chunk-QKM4U5UK.js → chunk-TJS2AF5Y.js} +2 -2
  39. package/dist/{chunk-DIA7CT7J.js → chunk-V5Y7D4LH.js} +11 -2
  40. package/dist/{chunk-KA6G2L7U.js → chunk-VK26K7AY.js} +3 -3
  41. package/dist/{chunk-VAF6A3YD.js → chunk-XWFRNJMQ.js} +1 -1
  42. package/dist/{chunk-7ZYW652H.js → chunk-YHS6VVLJ.js} +2 -2
  43. package/dist/{chunk-G4EJMQLD.js → chunk-YX22LKQE.js} +2 -2
  44. package/dist/{chunk-F6CYJ7TN.js → chunk-Z2ZIE5CO.js} +2 -2
  45. package/dist/cli.d.ts +13 -6
  46. package/dist/cli.js +10 -10
  47. package/dist/codegen-command-ZDHMGFWZ.js +47 -0
  48. package/dist/codegen.d.ts +5 -5
  49. package/dist/codegen.js +2 -2
  50. package/dist/{completion-WF46272M.js → completion-HJU5QEFB.js} +2 -2
  51. package/dist/{deploy-command-IP7V7GT4.js → deploy-command-CGVKRWUE.js} +19 -19
  52. package/dist/{ephemeral-command-U4AQ3TXX.js → ephemeral-command-2NKPEXUU.js} +8 -8
  53. package/dist/index.d.ts +88 -104
  54. package/dist/index.js +9 -9
  55. package/dist/init-command-DNDONP3O.js +32 -0
  56. package/dist/internal.d.ts +10 -16
  57. package/dist/internal.js +34 -34
  58. package/dist/{io-P2H75UV2.js → io-UBDMMDH6.js} +3 -3
  59. package/dist/{live-diff-IXKBVG4K.js → live-diff-HCOTN5WC.js} +3 -3
  60. package/dist/{lock-46FWYE4D.js → lock-HQ4KARU2.js} +2 -2
  61. package/dist/{lock-commands-ZZKZ4LZJ.js → lock-commands-6U7UIJGR.js} +11 -11
  62. package/dist/{login-command-Z6CHTA57.js → login-command-P7LXD5TE.js} +6 -6
  63. package/dist/{loop-D5NPL4VH.js → loop-IC5ISSYB.js} +3 -3
  64. package/dist/{marketplace-command-P4IPLJ6J.js → marketplace-command-T6JCHW7J.js} +4 -4
  65. package/dist/{meta-client-K2J4XH64.js → meta-client-LKRKR3L2.js} +4 -4
  66. package/dist/node.d.ts +6 -6
  67. package/dist/node.js +14 -14
  68. package/dist/{preflight-command-K346GPTY.js → preflight-command-UYE7SUQV.js} +16 -16
  69. package/dist/{profile-command-LJSBDV2L.js → profile-command-WBNSMQSI.js} +3 -3
  70. package/dist/{release-command-IMNTIVWK.js → release-command-CMMYT6XK.js} +26 -26
  71. package/dist/{response-BQVQ24l1.d.ts → response-D6xGLEIn.d.ts} +18 -23
  72. package/dist/{routes-manifest-PWZHDOI5.js → routes-manifest-MN6XBYRE.js} +25 -14
  73. package/dist/{runtime-V4C3AC3A.js → runtime-LSLIDALK.js} +1 -1
  74. package/dist/scaffold.d.ts +16 -8
  75. package/dist/scaffold.js +7 -11
  76. package/dist/{static-host-3WMV7IZO.js → static-host-4JDQCHUW.js} +1 -1
  77. package/dist/{status-command-AL47VG7H.js → status-command-VARULAR5.js} +4 -4
  78. package/dist/{store-BLyNeQ8S.d.ts → store-9Psd0jiF.d.ts} +115 -102
  79. package/dist/{test-command-YAZLKLGQ.js → test-command-YCAR4O35.js} +7 -7
  80. package/dist/{upgrade-command-BN3EHAOI.js → upgrade-command-A75DOIUH.js} +15 -16
  81. package/dist/{workspace-2COHDBM3.js → workspace-33KHFLX3.js} +2 -2
  82. package/dist/{workspace-command-MNK7Y7MQ.js → workspace-command-YELP47SJ.js} +26 -28
  83. package/dist/{workspace-export-DURY5WYL.js → workspace-export-MLYTYZS5.js} +3 -3
  84. package/dist/{xdo-BjJj5W_E.d.ts → xdo-ODuJklk6.d.ts} +27 -23
  85. package/guides/authoring.md +11 -2
  86. package/guides/typed-frontend.md +23 -6
  87. package/llms/kinds-realtime.md +2 -2
  88. package/llms/statements-data.md +3 -2
  89. package/llms/tests.md +1 -1
  90. package/llms/triggers.md +2 -2
  91. package/llms/values.md +1 -1
  92. package/llms-full.txt +26 -26
  93. package/llms.txt +17 -18
  94. package/manifest.json +6 -2
  95. package/package.json +1 -1
  96. package/dist/chunk-ANUDXFEX.js +0 -881
  97. package/dist/chunk-JGCWTCA7.js +0 -95
  98. package/dist/chunk-YBC3IKMF.js +0 -845
  99. package/dist/codegen-command-FUT2KJB6.js +0 -49
  100. package/dist/init-command-NPVL32L6.js +0 -34
@@ -4,25 +4,25 @@ import {
4
4
  } from "./chunk-NUQCEOKA.js";
5
5
  import {
6
6
  resolveMetaTarget
7
- } from "./chunk-ZQ2PKR6R.js";
7
+ } from "./chunk-RQ3FXV4K.js";
8
8
  import "./chunk-K5IOND4K.js";
9
9
  import {
10
10
  getAccessToken
11
- } from "./chunk-QKM4U5UK.js";
11
+ } from "./chunk-TJS2AF5Y.js";
12
12
  import "./chunk-FE5I6S6N.js";
13
13
  import {
14
14
  fetchOrExplain,
15
15
  httpFailure,
16
16
  serverMessage
17
- } from "./chunk-3IGNIP6R.js";
17
+ } from "./chunk-NDP7OUPS.js";
18
18
  import {
19
19
  parseEnvSelector,
20
20
  unknownEnv
21
- } from "./chunk-CTD5ZCV6.js";
21
+ } from "./chunk-7WRJPKGK.js";
22
22
  import {
23
23
  UsageError,
24
24
  unknownSubcommand
25
- } from "./chunk-F6CYJ7TN.js";
25
+ } from "./chunk-Z2ZIE5CO.js";
26
26
  import "./chunk-GNPVYOPB.js";
27
27
  import {
28
28
  blank,
@@ -32,7 +32,7 @@ import {
32
32
  success,
33
33
  warn
34
34
  } from "./chunk-EZG76F7R.js";
35
- import "./chunk-DIA7CT7J.js";
35
+ import "./chunk-V5Y7D4LH.js";
36
36
 
37
37
  // src/deploy/tests.ts
38
38
  var TIMEOUT_MS = 12e4;
@@ -397,4 +397,4 @@ export {
397
397
  runSuite,
398
398
  runTestCommand
399
399
  };
400
- //# sourceMappingURL=test-command-YAZLKLGQ.js.map
400
+ //# sourceMappingURL=test-command-YCAR4O35.js.map
@@ -3,30 +3,29 @@ import {
3
3
  } from "./chunk-ERQZFWIW.js";
4
4
  import {
5
5
  refreshAgentFiles
6
- } from "./chunk-JGCWTCA7.js";
6
+ } from "./chunk-2VTJSI6X.js";
7
7
  import {
8
8
  isMachineOutput,
9
9
  writeJson
10
10
  } from "./chunk-NUQCEOKA.js";
11
- import "./chunk-KA6G2L7U.js";
11
+ import "./chunk-VK26K7AY.js";
12
12
  import {
13
13
  runNpm
14
14
  } from "./chunk-ZSYZTGJH.js";
15
15
  import {
16
16
  sdkDep
17
- } from "./chunk-ANUDXFEX.js";
18
- import "./chunk-Q77KNEUL.js";
17
+ } from "./chunk-RQNMTDXD.js";
19
18
  import {
20
19
  checkForUpgrade,
21
20
  detectInstallMode,
22
21
  registryUrl,
23
22
  suppressUpdateNotice,
24
23
  upgradeCommand
25
- } from "./chunk-22TKBSDV.js";
26
- import "./chunk-RCT7UX7B.js";
27
- import "./chunk-CTD5ZCV6.js";
28
- import "./chunk-XEOX6AM7.js";
29
- import "./chunk-F6CYJ7TN.js";
24
+ } from "./chunk-2AY3PKF4.js";
25
+ import "./chunk-RLI6XD4O.js";
26
+ import "./chunk-7WRJPKGK.js";
27
+ import "./chunk-3LQGF2WS.js";
28
+ import "./chunk-Z2ZIE5CO.js";
30
29
  import "./chunk-GNPVYOPB.js";
31
30
  import {
32
31
  blank,
@@ -36,12 +35,12 @@ import {
36
35
  success,
37
36
  warn
38
37
  } from "./chunk-EZG76F7R.js";
39
- import "./chunk-OWGCOGKK.js";
40
- import "./chunk-DIA7CT7J.js";
41
- import "./chunk-QK7ZQJLP.js";
42
- import "./chunk-WHOJWOSV.js";
43
- import "./chunk-VAF6A3YD.js";
44
- import "./chunk-5XZ744TS.js";
38
+ import "./chunk-MEFMTICH.js";
39
+ import "./chunk-V5Y7D4LH.js";
40
+ import "./chunk-N74KDCBD.js";
41
+ import "./chunk-EETVJZAZ.js";
42
+ import "./chunk-XWFRNJMQ.js";
43
+ import "./chunk-2ZCUO2UG.js";
45
44
  import "./chunk-OHX6MIUZ.js";
46
45
 
47
46
  // src/emit/upgrade-command.ts
@@ -176,4 +175,4 @@ function messageOf(err) {
176
175
  export {
177
176
  runUpgradeCommand
178
177
  };
179
- //# sourceMappingURL=upgrade-command-BN3EHAOI.js.map
178
+ //# sourceMappingURL=upgrade-command-A75DOIUH.js.map
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  fetchReadOrExplain
3
- } from "./chunk-3IGNIP6R.js";
3
+ } from "./chunk-NDP7OUPS.js";
4
4
 
5
5
  // src/deploy/workspace.ts
6
6
  var RESOLVE_TIMEOUT_MS = 3e4;
@@ -46,4 +46,4 @@ ${text}`);
46
46
  export {
47
47
  resolveScopedWorkspaceId
48
48
  };
49
- //# sourceMappingURL=workspace-2COHDBM3.js.map
49
+ //# sourceMappingURL=workspace-33KHFLX3.js.map
@@ -11,41 +11,39 @@ import {
11
11
  } from "./chunk-NUQCEOKA.js";
12
12
  import {
13
13
  fetchWorkspaceBundle
14
- } from "./chunk-HYBN4H3F.js";
15
- import "./chunk-ZQ2PKR6R.js";
14
+ } from "./chunk-SS2V2QOG.js";
15
+ import "./chunk-RQ3FXV4K.js";
16
16
  import "./chunk-K5IOND4K.js";
17
17
  import {
18
18
  ENV_WORKSPACE,
19
19
  getAccessToken
20
- } from "./chunk-QKM4U5UK.js";
20
+ } from "./chunk-TJS2AF5Y.js";
21
21
  import "./chunk-FE5I6S6N.js";
22
- import "./chunk-7ZYW652H.js";
22
+ import "./chunk-YHS6VVLJ.js";
23
23
  import "./chunk-ZUTSMMAG.js";
24
24
  import "./chunk-X4DVXBFY.js";
25
25
  import {
26
26
  fetchOrExplain,
27
27
  httpFailure
28
- } from "./chunk-3IGNIP6R.js";
29
- import "./chunk-4IF54NU5.js";
30
- import "./chunk-KA6G2L7U.js";
28
+ } from "./chunk-NDP7OUPS.js";
29
+ import "./chunk-S3DOJOW4.js";
30
+ import "./chunk-VK26K7AY.js";
31
31
  import "./chunk-ZSYZTGJH.js";
32
- import "./chunk-G4EJMQLD.js";
33
- import "./chunk-LU7TRWMC.js";
34
- import "./chunk-YBC3IKMF.js";
35
- import "./chunk-ANUDXFEX.js";
36
- import "./chunk-Q77KNEUL.js";
37
- import "./chunk-BSK7ELHU.js";
38
- import "./chunk-UOZMSF4C.js";
32
+ import "./chunk-YX22LKQE.js";
33
+ import "./chunk-6VNRMKFJ.js";
34
+ import "./chunk-RQNMTDXD.js";
35
+ import "./chunk-PR7OXHGZ.js";
36
+ import "./chunk-BYQHCCYU.js";
39
37
  import "./chunk-EHP3WPEG.js";
40
- import "./chunk-3INK4Y4E.js";
41
- import "./chunk-BC2C5GVI.js";
42
- import "./chunk-RCT7UX7B.js";
43
- import "./chunk-CTD5ZCV6.js";
44
- import "./chunk-XEOX6AM7.js";
38
+ import "./chunk-AE3PDSDS.js";
39
+ import "./chunk-DCMANKMX.js";
40
+ import "./chunk-RLI6XD4O.js";
41
+ import "./chunk-7WRJPKGK.js";
42
+ import "./chunk-3LQGF2WS.js";
45
43
  import {
46
44
  removedSubcommand,
47
45
  unknownSubcommand
48
- } from "./chunk-F6CYJ7TN.js";
46
+ } from "./chunk-Z2ZIE5CO.js";
49
47
  import "./chunk-GNPVYOPB.js";
50
48
  import {
51
49
  formatFields,
@@ -53,12 +51,12 @@ import {
53
51
  step,
54
52
  success
55
53
  } from "./chunk-EZG76F7R.js";
56
- import "./chunk-OWGCOGKK.js";
57
- import "./chunk-DIA7CT7J.js";
58
- import "./chunk-QK7ZQJLP.js";
59
- import "./chunk-WHOJWOSV.js";
60
- import "./chunk-VAF6A3YD.js";
61
- import "./chunk-5XZ744TS.js";
54
+ import "./chunk-MEFMTICH.js";
55
+ import "./chunk-V5Y7D4LH.js";
56
+ import "./chunk-N74KDCBD.js";
57
+ import "./chunk-EETVJZAZ.js";
58
+ import "./chunk-XWFRNJMQ.js";
59
+ import "./chunk-2ZCUO2UG.js";
62
60
  import "./chunk-OHX6MIUZ.js";
63
61
 
64
62
  // src/emit/workspace-command.ts
@@ -72,7 +70,7 @@ async function runWorkspaceCommand(args) {
72
70
  case "export":
73
71
  return runExport(args);
74
72
  case "branch": {
75
- const { runBranchCommand } = await import("./branch-commands-2BLOC2GR.js");
73
+ const { runBranchCommand } = await import("./branch-commands-5KMPAAZV.js");
76
74
  return runBranchCommand(args);
77
75
  }
78
76
  case "deploy":
@@ -162,4 +160,4 @@ async function runExport(args) {
162
160
  export {
163
161
  runWorkspaceCommand
164
162
  };
165
- //# sourceMappingURL=workspace-command-MNK7Y7MQ.js.map
163
+ //# sourceMappingURL=workspace-command-YELP47SJ.js.map
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  exportWorkspaceBundle
3
- } from "./chunk-7ZYW652H.js";
3
+ } from "./chunk-YHS6VVLJ.js";
4
4
  import "./chunk-ZUTSMMAG.js";
5
5
  import "./chunk-X4DVXBFY.js";
6
- import "./chunk-3IGNIP6R.js";
6
+ import "./chunk-NDP7OUPS.js";
7
7
  export {
8
8
  exportWorkspaceBundle
9
9
  };
10
- //# sourceMappingURL=workspace-export-DURY5WYL.js.map
10
+ //# sourceMappingURL=workspace-export-MLYTYZS5.js.map
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The shared tagged-value primitive (KTD-2). Every place a function references
2
+ * The shared tagged-value primitive. Every place a function references
3
3
  * data — input bindings, statement context, response — uses this `{value, tag,
4
4
  * filters}` shape. Built and tested once here, reused everywhere.
5
5
  */
@@ -10,7 +10,7 @@ type Value = TaggedValue;
10
10
  * A {@link Value} that also carries, **at the type level only**, the name of the
11
11
  * stack variable it references (`ref("user")` → `RefValue<"user">`). The `__ref`
12
12
  * carrier is phantom — never present at runtime — and required (not optional) so
13
- * `InferResponse`'s trace (U5) matches only real refs, never a plain `Value`.
13
+ * `InferResponse`'s trace matches only real refs, never a plain `Value`.
14
14
  * Because it is a subtype of `Value`, every existing `ref(...)` use — filter
15
15
  * args, `db.query` `where`, response fields — keeps type-checking unchanged.
16
16
  */
@@ -41,7 +41,7 @@ type FilteredValue<Base = unknown, Chain extends readonly unknown[] = readonly u
41
41
  * an editor's hover distinguishes a pattern from the text beside it.
42
42
  *
43
43
  * It is deliberately NOT used to restrict `s.expect.to_match` or the `fl.regex_*`
44
- * pattern slot, which is what issue #213 proposed. A nominal restriction there
44
+ * pattern slot. A nominal restriction there
45
45
  * would have to reject `Value`, and `inp()`, `env()`, `auth()` and `sys.*` all
46
46
  * return exactly `Value` — the same type `c.text` does — so it would reject every
47
47
  * DYNAMIC pattern along with the broken constant ones. The enforcement is instead
@@ -63,15 +63,15 @@ type FlattenFilters<Fs extends readonly unknown[]> = Fs extends readonly [
63
63
  * *statically rejected* where it would silently fail at runtime: inside a
64
64
  * `db.edit`/`db.add` `row`, `{tag:"col"}` does not resolve to the row's stored
65
65
  * value — it evaluates to `null`, so a following `fl.add(1)` computes `null + 1`
66
- * and the engine aborts ("Numbers are required for mathematical operations",
67
- * issue #32). `col()` is only meaningful in a `db.query` `where`/view expression.
66
+ * and the engine aborts ("Numbers are required for mathematical operations").
67
+ * `col()` is only meaningful in a `db.query` `where`/view expression.
68
68
  */
69
69
  type ColValue = Value & {
70
70
  readonly __col: true;
71
71
  };
72
72
  /**
73
73
  * The error branch surfaced when a tagged {@link Value} is nested inside a
74
- * `c.obj`/`c.array` literal (issue #42). The long message is the *property key*
74
+ * `c.obj`/`c.array` literal. The long message is the *property key*
75
75
  * so TypeScript prints it verbatim in the "property … is missing" diagnostic; a
76
76
  * `Value` has no such key, so intersecting it here makes the offending position
77
77
  * fail to type-check. The runtime guard ({@link assertPlainJson}) carries the
@@ -107,7 +107,7 @@ type BlankTag = Exclude<Extract<Tag, `const${string}`>, "const" | "const:obj">;
107
107
  /**
108
108
  * Tags {@link c.null} can spell — the two the engine stores a `"null"` value
109
109
  * under. `const:null` is the plain one; `const:obj` is the object-typed null a
110
- * `db.*` statement's `@meta` slot carries (issue #151).
110
+ * `db.*` statement's `@meta` slot carries.
111
111
  *
112
112
  * Listed rather than derived: a `"null"` value under any OTHER constant tag is
113
113
  * unobserved, and inventing spellings for it would offer an authoring form for
@@ -141,7 +141,7 @@ declare const c: {
141
141
  * A `number` that is not an integer, or is past `Number.MAX_SAFE_INTEGER`,
142
142
  * **throws**: by the time such a literal reaches here its precision is already
143
143
  * gone, so encoding it would write a value the caller did not type. The throw
144
- * names the string form. (issue #153)
144
+ * names the string form.
145
145
  */
146
146
  int(n: number | bigint | string): Value;
147
147
  /**
@@ -181,7 +181,7 @@ declare const c: {
181
181
  * null** the engine writes into a `db.*` statement's `@meta` slot —
182
182
  * `c.null("const:obj")` → `{tag:"const:obj", value:"null"}`, 16 of them across
183
183
  * eight ordinary queries in the survey corpus, which had no spelling and came
184
- * back as `rawValue` (issue #151).
184
+ * back as `rawValue`.
185
185
  *
186
186
  * ⚠ Not the same bytes as `c.obj(null)`, which is the BLANK object
187
187
  * (`value: ""`). Both evaluate to null — the engine JSON-decodes the stored
@@ -195,7 +195,7 @@ declare const c: {
195
195
  * (`fl.regex_test`/`regex_match`/`regex_replace`/…). Xano runs PHP `preg_*`, so
196
196
  * the pattern MUST be delimiter-wrapped — a bare `c.text("^…$")` is an invalid
197
197
  * PCRE and the filter then matches *nothing* for every input, so a precondition
198
- * built on it silently rejects all values (issue #128). This wraps the raw
198
+ * built on it silently rejects all values. This wraps the raw
199
199
  * pattern in `/…/`, escapes any interior `/`, and appends `flags`, so the result
200
200
  * is always valid; `withFilters` rejects a bare `c.text` pattern and points here.
201
201
  *
@@ -216,8 +216,13 @@ declare const c: {
216
216
  * `text("now") |to_epoch_ms` chain; both evaluate to the same epoch-ms number
217
217
  * on a live engine, and both persist verbatim, but the native tag is what the
218
218
  * editor writes and needs no filter to get there. Chain math onto it as usual:
219
- * `withFilters(c.now(), fl.epochms_add_ms(c.int(-maxAgeMs)))`. (issues #120,
220
- * #145)
219
+ * `withFilters(c.now(), fl.epochms_add_ms(c.int(-maxAgeMs)))`.
220
+ *
221
+ * Inside an `obj()` the tag is flattened into an expression string, where the
222
+ * identifier `now` alone is the STRING "now" — so `obj()` writes the
223
+ * `now|to_epochms` coercion that restores this tag's meaning. A `c.now()` used
224
+ * as a FILTER ARGUMENT there is refused: the coercion cannot be written in
225
+ * that position. Hoist it with `s.set_var` and `ref()` it.
221
226
  */
222
227
  now(): Value;
223
228
  /**
@@ -226,7 +231,6 @@ declare const c: {
226
231
  * compile error and throws at runtime — it would serialize as internal
227
232
  * representation the engine can't decode. For a computed/multi-key object
228
233
  * response use a record of values (`response: { key: value }`), not `c.obj`.
229
- * See issue #42.
230
234
  *
231
235
  * Called with no argument it is the **empty object**, `{}` — the same default
232
236
  * the editor gives a new object variable, and the only empty form current
@@ -243,7 +247,7 @@ declare const c: {
243
247
  * same bytes instead of being quietly re-pointed at `{}`.
244
248
  *
245
249
  * A **populated** object is stored the way the editor stores one: an empty
246
- * `{}` base carrying one `set` filter per key (issue #248). It is NOT a
250
+ * `{}` base carrying one `set` filter per key. It is NOT a
247
251
  * populated JSON string — that form fails the request at runtime, see
248
252
  * {@link objSetFilters}.
249
253
  *
@@ -257,7 +261,7 @@ declare const c: {
257
261
  /**
258
262
  * Array constant → JSON-string value with `tag:"const:array"`. Takes **plain
259
263
  * JSON literals only** — like {@link obj}, a nested tagged value is a compile
260
- * error and throws at runtime. See issue #42.
264
+ * error and throws at runtime.
261
265
  */
262
266
  array<const T extends readonly unknown[]>(a: T & RejectValues<T>): Value;
263
267
  /**
@@ -308,7 +312,7 @@ interface RefOptions {
308
312
  * compiles to the raw var path `$owner.user_id`, which the engine resolves in
309
313
  * a single lookup — so when the base var `owner` is null (e.g. a `db.get` that
310
314
  * matched no row), it raises a runtime `ERROR_FATAL` "Unable to locate var"
311
- * (HTTP 500) instead of yielding null (issue #47).
315
+ * (HTTP 500) instead of yielding null.
312
316
  *
313
317
  * With `safe: true` the path compiles through the `get` filter
314
318
  * (`$owner|get:"user_id"`), which walks the remaining path and resolves to
@@ -324,7 +328,7 @@ interface RefOptions {
324
328
  *
325
329
  * Pass `{ safe: true }` to make a *nested* path null-safe: `ref("owner.user_id",
326
330
  * { safe: true })` resolves to null instead of 500ing when the base `owner` is
327
- * null (issue #47) — the intent-revealing opt-in for drilling into a `db.get`
331
+ * null — the intent-revealing opt-in for drilling into a `db.get`
328
332
  * result that may not exist.
329
333
  *
330
334
  * Picking a reference helper (these are easy to mix up):
@@ -339,7 +343,7 @@ declare function ref<const Name extends string>(name: Name, opts?: RefOptions):
339
343
  /**
340
344
  * The second parameter every non-`ref` reference creator declares, so that
341
345
  * passing `{ safe: true }` to one is a COMPILE error whose expected type is the
342
- * explanation (issue #80).
346
+ * explanation.
343
347
  *
344
348
  * `safe` was silently dropped before this: the creators took one parameter, so
345
349
  * JavaScript discarded the extra argument and TypeScript never saw it. An
@@ -353,7 +357,7 @@ declare function inp(name: string, opts?: SafeIsRefOnly): Value;
353
357
  * Reference a table **column**: `{tag:"col", value}` (used in `db.query` `where` +
354
358
  * table views). See {@link ref} for the full picker. The return is branded
355
359
  * {@link ColValue} so it is a *compile error* to pass `col()` into a `db.edit`/
356
- * `db.add` `row` — where it would resolve to `null` at runtime (issue #32).
360
+ * `db.add` `row` — where it would resolve to `null` at runtime.
357
361
  */
358
362
  declare function col(name: string, opts?: SafeIsRefOnly): ColValue;
359
363
  /**
@@ -422,7 +426,7 @@ type ToolsetPath = "token" | "params" | `params.${string}`;
422
426
  * - `toolset("params.tenant")` — one parameter out of that object.
423
427
  *
424
428
  * Only bound while a tool runs under its toolset; anywhere else it reads empty
425
- * (issue #152).
429
+ *.
426
430
  */
427
431
  declare function toolset(path: ToolsetPath, opts?: SafeIsRefOnly): Value;
428
432
  /**
@@ -745,7 +749,7 @@ declare function test(def: TestDef): TestDef;
745
749
  * The SDK emits only authored fields. Server/auto-generated keys (`id`,
746
750
  * `created_at`, `_xsid`, `@guid`, `@index`, `guid`, `_draft`, `stack_id`, …)
747
751
  * are generated by the engine on import and are NEVER emitted here; the test
748
- * normalizer (U6) strips them from the fixture before comparison.
752
+ * normalizer strips them from the fixture before comparison.
749
753
  */
750
754
  /**
751
755
  * Xano tagged-value tags (verified against the Xano engine). The runtime
@@ -779,7 +783,7 @@ interface FilterXdo<N extends string = string> {
779
783
  * Required here until now, which made a pulled workspace fail its own
780
784
  * type-check: codegen emits a stored filter verbatim when no `fl.*` form
781
785
  * reproduces it, and a verbatim emit of a filter stored without the key does
782
- * not have it (issue #134). Writing `disabled: false` in would have been the
786
+ * not have it. Writing `disabled: false` in would have been the
783
787
  * other repair, and it is the wrong one — it ADDS a key the engine did not
784
788
  * store, so the re-encode would no longer match the stored bytes and the value
785
789
  * would degrade further rather than round-trip.
@@ -810,7 +814,7 @@ interface MethodXdo {
810
814
  }
811
815
  /**
812
816
  * A field-bearing entry — the shape shared by function/query inputs and table
813
- * columns (KTD-6). In the persisted form the two are byte-identical: both use
817
+ * columns. In the persisted form the two are byte-identical: both use
814
818
  * `customize:{}`, numeric `market_item` ids, an `_xsid` placeholder, and a
815
819
  * present `description`. (The older parser generation emitted `customize:""` /
816
820
  * string ids for inputs; the live `mvp_query`/`mvp_dbo` rows show they converge.)
@@ -137,6 +137,13 @@ auto-assigned `id`/`created_at`, `s.db.del` binds `null`, and `edit`/`del` **thr
137
137
  the engine reads the operand as a text literal and fails at runtime with a parse error
138
138
  naming the *other* operand, so `db.query` rejects that spelling at export instead.
139
139
 
140
+ - **A join adds no columns to the returned row.** With or without a `bind`, a row is the
141
+ queried table's columns — which is what `InferResponse` types, so there is no `row.author`
142
+ to read. To bring a joined column back, project it with an `eval` whose `name` is the dotted
143
+ path: `eval: [{ name: "author.name", as: "author_name" }]` puts `author_name` on the row and
144
+ on the inferred type. A *bare* name there fails at runtime (it qualifies to the base table),
145
+ and a dotted joined column in `output` is dropped with no error.
146
+
140
147
  - **Paging changes the response shape.** Supplying `paging` with metadata on (the default)
141
148
  returns a **paging envelope** — `{ items, curPage, nextPage, prevPage, offset, perPage,
142
149
  itemsReceived }`, plus totals when `totals: true` — instead of a bare `Row[]`, and
@@ -194,8 +201,10 @@ auto-assigned `id`/`created_at`, `s.db.del` binds `null`, and `edit`/`del` **thr
194
201
  `lower`, and the `epochms_add_day` / `epochms_sub_month` family). The request-time timestamp
195
202
  filters do not — `epochms_transform`, `epochms_add_ms`, `epochms_add_secs`, `epochms_date`,
196
203
  `epochms_from_format` — and the engine doesn't degrade gracefully: the request dies with a
197
- bare fatal naming nothing. For a relative cutoff, use the SQL-side spelling or compute it in
198
- an earlier `s.set_var` and `ref()` that. `export()` warns; a bare `c.now()` is always fine.
204
+ bare fatal naming nothing. For a relative cutoff, use the SQL-side spelling by raw name,
205
+ since `fl.*` carries no builder for that family: `filter("epochms_add_day", …)` or the raw
206
+ `{ name, arg }` form — or compute it in an earlier `s.set_var` and `ref()` that. `export()`
207
+ warns; a bare `c.now()` is always fine.
199
208
  - **A `s.switch` case without `break: true` falls through.** The engine's default is
200
209
  fallthrough, so a matched case also runs every *later* case body — and the `default` block
201
210
  too. Whatever those bodies write gets written two or three times, at HTTP 200, with `tsc`
@@ -157,10 +157,10 @@ also pulls whatever its `stack` builds — the `s.*`/`c.*` factory *calls* run a
157
157
  to construct the def, so they can't be tree-shaken out. Types are free (`InferInput`/
158
158
  `InferRow` erase to nothing — use `import type`). That cost is a **floor**, not a function of
159
159
  how lean the def is. Measured on a Vite lib build against the published package: one
160
- `apiGroup` + one empty `query`, imported for a single `getPath()`, is **289 kB minified
161
- (57 kB gzipped)** against 56 B for a hand-written path string. A realistic def — two
160
+ `apiGroup` + one empty `query`, imported for a single `getPath()`, is **267 kB minified
161
+ (65 kB gzipped)** against 56 B for a hand-written path string. A realistic def — two
162
162
  tables, a foreign key, a typed input, a `db.query` with a `where` and a sort, plus a
163
- second endpoint — measures 290 kB. That 1 kB spread is the point: the floor is the SDK
163
+ second endpoint — measures 269 kB. That 2 kB spread is the point: the floor is the SDK
164
164
  runtime itself, so splitting modules or simplifying a def does not move it, and the cost
165
165
  is paid by importing any def at all.
166
166
 
@@ -172,15 +172,32 @@ xanots routes ./xano/index.ts --emit xano/routes.gen.ts
172
172
  ```
173
173
 
174
174
  The emitted file is plain data plus one interpolator and imports nothing at all — the same
175
- app builds to 1.3 kB, a 214x saving, with no SDK code in the output. Route names and their `{param}` keys are still checked at compile
176
- time, so a backend rename is a compile error rather than a 404:
175
+ app builds to 1.3 kB, a ~200x saving, with no SDK code in the output. Route keys and their
176
+ `{param}` names are still checked at compile time, so a backend rename is a compile error
177
+ rather than a 404:
177
178
 
178
179
  ```ts
179
180
  import { routePath, ROUTES } from "../xano/routes.gen";
180
181
 
181
- fetch(BASE + routePath("blog/{slug}", { slug }), { method: ROUTES["blog/{slug}"].verb });
182
+ fetch(BASE + routePath("GET blog/{slug}", { slug }), {
183
+ method: ROUTES["GET blog/{slug}"].verb,
184
+ });
185
+ ```
186
+
187
+ A route key is `"<VERB> <name>"`, which is the endpoint identity the engine itself uses —
188
+ so verb-differentiated siblings are ordinary, and a REST-shaped group emits without
189
+ renaming anything:
190
+
191
+ ```ts
192
+ routePath("GET listings"); // list
193
+ routePath("POST listings"); // create
194
+ routePath("PATCH listings/{id}", { id }); // update
182
195
  ```
183
196
 
197
+ The verb comes first so the key is stable: adding a `POST` sibling never renames the `GET`
198
+ that was already there. Two api groups holding the same verb AND name is the one shape a
199
+ single manifest cannot express, and the emit fails naming both rather than picking one.
200
+
184
201
  Realtime is in the same file when the workspace has any: `socketUrl(server, baseUrl)` for the
185
202
  websocket URL and `channelPath(channel, params)` for the path a frame's `channel` field takes,
186
203
  both keyed and `{param}`-checked exactly like the routes. `socketUrl` is the equivalent of
@@ -20,11 +20,11 @@
20
20
  - `deliverTo?`: `"channel"` (default) | `"sender"` | `"others"` | `"explicit"`. ⚠ `"explicit"` still delivers to NOBODY — nothing selects recipients from inside a handler, and `s.realtime.publish` (which originates an event INTO a channel) is not a substitute.
21
21
  - Only `"channel"`/`"others"` fan out AND are written to the `conversation` transcript — a `"sender"` response is invisible to every future joiner.
22
22
  - **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.
23
- - A path param is bound ONCE at join and read from the connection thereafter, never from the frame — a sender cannot claim a room it did not join. The same values reach a channel `join`/`leave` trigger's stack.
23
+ - A path param is bound ONCE at join and read from the connection thereafter, never from the frame — a sender cannot claim a room it did not join.
24
24
  - `s.realtime.get_session({ as })` — the CALLER's realtime session for the current frame. FLAT shape:
25
25
  - `authenticated` bool · `client_id` text (the AUTHED ROW ID as text, `""` anonymous) · `dbo_id` int (the auth TABLE's id — NOT the user's row id; `0` anonymous — 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) · `socket_id` int (transport id) · `channel` text (resolved path, `""` in a server trigger) · `params` object (bound path params, `{}` when none — `ref("session.params.room_id")`) · `extras` object · `opened_at` decimal.
26
26
  - Works in a realtime MESSAGE stack and in CHANNEL and SERVER trigger stacks; off that path it degrades to an anonymous session.
27
- - For a path param prefer `inp("room_id")`. Reach for the session when you need the CONNECTION (identity/extras) — "who is this sender" on an anonymous-client channel.
27
+ - 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) — "who is this sender" on an anonymous-client channel.
28
28
  - ⚠ 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.
29
29
  - `s.realtime.publish({ server, channel, data, message?, authTable?, authId? })` — the PUSH direction: originate a server-authored event onto a channel from ANY stack, no client frame first.
30
30
  - `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 — a constant still carrying `{param}` THROWS at author time, and a constant `server`/`channel` naming nothing this workspace registers WARNS at export.
@@ -34,13 +34,14 @@ primary key `id`):
34
34
  - ⚠ `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 — nothing reports a problem, so a search box that matches nothing ships. For substring matching use `includes`/`not includes`, which wrap the operand in `%…%` 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 — on a text column they 400 `ParseError: Invalid value for param`.
35
35
  - Compose nested boolean logic with `and(...)` / `or(...)` groups (also available on `addon()` `where`).
36
36
  - An operand may be a bare value (`col`/`inp`/`ref`/`auth`/`c.*`) OR a **filtered** value (`withFilters(...)`) inline — the engine compiles the string and arithmetic filters (`trim`, `concat`, `upper`, `lower`, …) into the SQL.
37
- - ⚠ 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 (`epochms_add_day`, `epochms_sub_month`, `epochms_year`, …) or compute it in an earlier `s.set_var` and `ref()` that. `export --strict` reports it; a bare `c.now()` operand is always fine.
37
+ - ⚠ 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 (`epochms_add_day`, `epochms_sub_month`, `epochms_year`, …) — by raw name, since `fl.*` has no builder for them — or compute it in an earlier `s.set_var` and `ref()` that. `export --strict` reports it; a bare `c.now()` operand is always fine.
38
38
  - `bind: [{ table, as?, join?, where? }]` — joins (`context.bind[]`). `join` defaults to `"inner"`. `as` defaults to the table name; two joins to the same table need distinct aliases.
39
39
  - ⚠ 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`) — 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.
40
40
  - `bind: [{ table: team, as: "team_row", join: "left", where: expr(col("team"), "=", col("team_row.id")) }]`
41
+ - ⚠ A join does NOT put the joined table's columns on the returned row — 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.
41
42
  - `returnType` — `"list"` (default) | `"single"` | `"count"` | `"exists"` | `"stream"` | `"aggregate"`. Drives `context.return.type` AND the `InferResponse` shape: `count`→`number`, `exists`→`boolean`, `single`→`Row|null`, `stream`→`Row[]` (pageable, no envelope), `list`→`Row[]`/envelope, `aggregate`→rows keyed by the `aggregate.group`/`eval` aliases. ⚠ A bare `count` of ZERO serializes as an EMPTY body, not `0` — a client parsing JSON gets a parse error on the one result it most needs to handle. Wrap it: `response: { count: ref("n") }`.
42
43
  - `eval: [{ name, as, filters? }]` — 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"`) — 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.
43
- - An `eval`/`sort`/`where` filter pipeline compiles to **SQL**, so it resolves a DIFFERENT registry than `fl.*` (which runs in the request): the vector family, geo `distance`/`within`/`covers`, `search_rank`, the aggregators. Exported as `QUERY_EXPRESSION_FILTERS`/`VECTOR_FILTERS`.
44
+ - An `eval`/`sort`/`where` filter pipeline compiles to **SQL**, so it resolves a DIFFERENT registry than `fl.*` (which runs in the request): the vector family, geo `distance`/`within`/`covers`, `search_rank`, the aggregators. `fl.*` carries NO builder for these — reach them by raw name, `filter("epochms_add_day", …)` or the `{ name, arg }` form. The name lists are `QUERY_EXPRESSION_FILTERS`/`VECTOR_FILTERS` on `@xanots/sdk/internal`.
44
45
  - **Vector similarity search** — 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`↔`vector_cosine_ops`, `vector_l2_distance`↔`vector_l2_ops`, `vector_l1_distance`↔`vector_l1_ops`, `vector_inner_product`↔`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.
45
46
  - `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`.
46
47
  - ⚠ Write each `name` as a **bare** column (`"status"`). It is alias-qualified to `"<alias>.status"` on emit — the engine rejects an unqualified column in an aggregate with `Unsupported param format`. An already-dotted `name` (a `bind`ed/joined column) passes through.
package/llms/tests.md CHANGED
@@ -42,7 +42,7 @@ The run is isolated in ways that make a correct test fail for reasons the failur
42
42
 
43
43
  - The run uses an EMPTY datasource, so **no `table({ seed })` rows exist while it runs** and every `db` read misses. A test that buys seeded row 1 fails with its own precondition message, which reads as a wrong id rather than an empty database. Create what the test needs INSIDE the test — typically a `defineFunction` fixture the stack calls first.
44
44
  - `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 — `s.expect.to_contain({ expr: ref("r.code"), value: c.text("ERROR_CODE_INPUT_ERROR") })` — when a call may fail. `s.function.run` raises instead; the two disagree.
45
- - `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 — bind what the body needs inside the body. `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.
45
+ - `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 — 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.
46
46
  - `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` — which reads as a broken auth gate on a gate that works.
47
47
  - 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.
48
48
  - Neither `auth.token` nor an `Authorization` entry in `headers` authenticates the call — 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.
package/llms/triggers.md CHANGED
@@ -15,8 +15,8 @@ realtimeChannelTrigger, mcpServerTrigger, agentTrigger, workspaceTrigger,
15
15
  errorTrigger}({ name, guid?, description?, active?, tags?, ... })`.
16
16
 
17
17
  - `tableTrigger({ name, table?, datasources?, actions?: {insert?,update?,delete?,truncate?}, stack })` — 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). Nullability follows the enabled actions: insert → `old` is null, delete → `new` is null, update → both, truncate → neither. Config-only (no response).
18
- - `realtimeServerTrigger({ name, realtimeServer, actions?: {connect?,disconnect?}, stack?, response?, responseShape? })` — 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). `connect` GATES the connection — 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` (`{ allowed: c.bool(true) }` or any truthy value admits, EMPTY/FALSY DENIES — INCLUDING a gating trigger with NO `response`, which returns nothing and so refuses every client). A CRASH DENIES too — the transport seeds a deny and keeps it on a throw. Both failure modes lock the door, so plan for a self-inflicted LOCKOUT (an unguarded drill into a null `db.get` raises → everyone refused), not a breach. Gating is OPT-IN: a server with no `connect` trigger accepts every connection. `disconnect` is OBSERVATIONAL (return ignored, throws swallowed — cleanup must always complete). Both are SERVER-scoped, so `s.realtime.get_session` works but carries no channel path and no bound params.
19
- - `realtimeChannelTrigger({ name, channel, actions?: {join?,leave?,deliver?}, stack?, response?, responseShape? })` — realtime CHANNEL lifecycle. Inputs: `t.action` (`join|leave|deliver`), `t.channel`, `t.client`. Bind `channel` to a `realtimeChannel()` handle — 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: `join` GATES the join (it runs BEFORE the client becomes a member, so a denial means it never sees a fan-out) — 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 — or a gating trigger with NO `response` — refuses everyone, and a CRASH DENIES too. That is the inverse of a normal message (a crashing message still delivers) and of `deliver` below (a gate that fails OPEN). `join`/`leave` bind the channel's typed path params as INPUTS, so `inp("room_id")` resolves and the gate decides per room; a SERVER connect/disconnect has no channel, the one place a path param cannot be read; `leave` is OBSERVATIONAL (return ignored, throws swallowed); `deliver` GATES delivery PER RECIPIENT — 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 — 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. **`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 — INCLUDING `false`, `0`, `""` — DELIVERS IT UNCHANGED, as does a crash. So `return false` from a yes/no redaction check SENDS the message it was written to suppress — return null instead. The delivered payload arrives NESTED, so read `inp("payload").<field>`, and `t.client` is the SENDER while `s.realtime.get_session` describes the RECIPIENT this run is for.
18
+ - `realtimeServerTrigger({ name, realtimeServer, actions?: {connect?,disconnect?}, stack?, response?, responseShape? })` — 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). `connect` GATES the connection — 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 — INCLUDING a gating trigger with NO `response`, which returns nothing and so refuses every client). A CRASH DENIES too — 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 → everyone refused), not a breach. Gating is OPT-IN: a server with no `connect` trigger accepts every connection. `disconnect` is OBSERVATIONAL (return ignored, throws swallowed — cleanup must always complete). Both are SERVER-scoped, so `s.realtime.get_session` works but carries no channel path and no bound params.
19
+ - `realtimeChannelTrigger({ name, channel, actions?: {join?,leave?,deliver?}, stack?, response?, responseShape? })` — realtime CHANNEL lifecycle. Inputs: `t.action` (`join|leave|deliver`), `t.channel`, `t.payload`, `t.client`. Bind `channel` to a `realtimeChannel()` handle — 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: `join` GATES the join (it runs BEFORE the client becomes a member, so a denial means it never sees a fan-out) — 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 — or a gating trigger with NO `response` — refuses everyone, and a CRASH DENIES too. ONCE the object carries an `allowed` key admission needs STRICTLY `true` — a computed `1`/`"yes"` there DENIES. That is the inverse of a crashing message, which still delivers, and of `deliver` below. A lifecycle trigger's inputs are PINNED to those four, so a channel PATH PARAM is NOT among them — `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")`). A gate establishes NO auth, so `ref("auth.id")` reads 0 even when authenticated identity is `t.client("permissions.dbo_id")` or the session. A SERVER connect/disconnect has no channel, so no params at all; `leave` is OBSERVATIONAL (return ignored, throws swallowed); `deliver` GATES delivery PER RECIPIENT — 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 — 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. **`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 — INCLUDING `false`, `0`, `""` — DELIVERS IT UNCHANGED, as does a crash. So `return false` from a yes/no redaction check SENDS the message it was written to suppress — return null instead. 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.
20
20
  - `mcpServerTrigger({ name, mcpServer, stack?, response?, responseShape? })` / `agentTrigger({ name, agent, stack?, response?, responseShape? })` — toolset connection. Bind with the `mcpServer()`/`agent()` def handle (or its name) — 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`. Response-bearing; the default stack copies `toolset`/`tools` into vars and returns them.
21
21
  - `workspaceTrigger({ name, actions?: {branch_live?,branch_merge?,branch_new?}, stack? })` — branch lifecycle. Inputs: `t.to_branch`, `t.from_branch`, `t.action`. Config-only.
22
22
  - `errorTrigger({ name, stack? })` — 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.
package/llms/values.md CHANGED
@@ -12,7 +12,7 @@
12
12
  - `c.array(a: Json[]) => Value` — Array constant (JSON string) → tag "const:array". Plain JSON literals only — a nested tagged value is rejected, same as c.obj.
13
13
  - `c.expression(source: string) => Value` — Xano Expression Engine source, passed through VERBATIM → tag "const:expr2". The string IS the expression: c.expression('"Hi, " ~ $input.name'), c.expression("$var.price * $var.qty"). ⚠️ NOT VALIDATED — 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) — prefer ref/inp/col, withFilters+fl.*, and obj() (which BUILDS a checked expression). Not the expr() condition builder.
14
14
  - `c.now() => Value` — Current time as epoch-ms — 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.
15
- - `obj(fields: Record<string, Value | nested>) => ObjValue<typeof fields>` — Dynamic object value → tag "const:expr2" (an object-literal expression string). The dynamic sibling of c.obj: members may be inp/ref/auth/col values, env()/setting()/sys.*, c.now(), c.* constants, nested records, or arrays — and each member may carry a FILTER CHAIN (withFilters + fl.*), which renders as the expression pipe `$var.row|get:"a.b"`. That matters most for the null-safe drill: db.get binds null on a miss, so ref(path, { safe: true }) inside an obj() is the normal shape, not a workaround — you do NOT need a preceding s.set_var to hoist it. The member record rides the return type, so InferResponse resolves each member the way it resolves a top-level response key — `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([...])` — reach for the bare form, it is shorter. NEST WITH A RAW RECORD, not an inner obj() call: an inner call yields a const:expr2 value, which the expression serializer has no spelling for and THROWS. The legacy blank `c.obj(null)` is refused here (it evaluates to null, not {}) — write c.null() or c.obj(). Still rejected: a filter ARGUMENT carrying its own chain (a trailing | binds to the whole value, not one argument), a DISABLED filter (an expression string cannot record that), and the output/response/toolset/reg tags — build those in a prior step and ref() them. Use for e.g. s.ai.agent.run args.
15
+ - `obj(fields: Record<string, Value | nested>) => ObjValue<typeof fields>` — Dynamic object value → tag "const:expr2" (an object-literal expression string). The dynamic sibling of c.obj: members may be inp/ref/auth/col values, env()/setting()/sys.*, c.now(), c.* constants, nested records, or arrays — and each member may carry a FILTER CHAIN (withFilters + fl.*), which renders as the expression pipe `$var.row|get:"a.b"`. That matters most for the null-safe drill: db.get binds null on a miss, so ref(path, { safe: true }) inside an obj() is the normal shape, not a workaround — you do NOT need a preceding s.set_var to hoist it. The member record rides the return type, so InferResponse resolves each member the way it resolves a top-level response key — `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([...])` — prefer the bare form. NEST WITH A RAW RECORD, not an inner obj() call: an inner call yields a const:expr2 value, which the expression serializer has no spelling for and THROWS. The legacy blank `c.obj(null)` is refused here (it evaluates to null, not {}) — 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/toolset/reg tags — build those in a prior step and ref() them. Use for e.g. s.ai.agent.run args.
16
16
  - `ref(name: string, opts?: { safe?: boolean }) => Value` — Reference a stack variable → tag "var". Pass { safe: true } for null-safe nested access — 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.
17
17
  - `inp(name: string) => Value` — Reference a function/endpoint input → tag "input". Resolves ONLY against the `input` block of the def it sits in — 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 — 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`).
18
18
  - `col(name: string) => Value` — Reference a table column → tag "col".