@tai42/studio-sdk 15.0.0 → 16.0.1

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 (322) hide show
  1. package/dist/components/explorer-body.d.ts +49 -0
  2. package/dist/components/explorer-body.d.ts.map +1 -0
  3. package/dist/components/explorer-body.js +67 -0
  4. package/dist/components/explorer-body.js.map +1 -0
  5. package/dist/components/explorer-page-size.d.ts +4 -0
  6. package/dist/components/explorer-page-size.d.ts.map +1 -0
  7. package/dist/components/explorer-page-size.js +36 -0
  8. package/dist/components/explorer-page-size.js.map +1 -0
  9. package/dist/components/explorer-tag-filter.d.ts +17 -0
  10. package/dist/components/explorer-tag-filter.d.ts.map +1 -0
  11. package/dist/components/explorer-tag-filter.js +29 -0
  12. package/dist/components/explorer-tag-filter.js.map +1 -0
  13. package/dist/components/explorer-tags.d.ts +22 -0
  14. package/dist/components/explorer-tags.d.ts.map +1 -0
  15. package/dist/components/explorer-tags.js +51 -0
  16. package/dist/components/explorer-tags.js.map +1 -0
  17. package/dist/components/explorer-view.d.ts +3 -17
  18. package/dist/components/explorer-view.d.ts.map +1 -1
  19. package/dist/components/explorer-view.js +53 -170
  20. package/dist/components/explorer-view.js.map +1 -1
  21. package/dist/components/extension-combo-builder.d.ts +0 -5
  22. package/dist/components/extension-combo-builder.d.ts.map +1 -1
  23. package/dist/components/extension-combo-builder.js +4 -42
  24. package/dist/components/extension-combo-builder.js.map +1 -1
  25. package/dist/components/extension-combo-parts.d.ts +44 -0
  26. package/dist/components/extension-combo-parts.d.ts.map +1 -0
  27. package/dist/components/extension-combo-parts.js +57 -0
  28. package/dist/components/extension-combo-parts.js.map +1 -0
  29. package/dist/components/fleet-report.d.ts.map +1 -1
  30. package/dist/components/fleet-report.js +51 -37
  31. package/dist/components/fleet-report.js.map +1 -1
  32. package/dist/components/icons/chrome-marks.d.ts +50 -0
  33. package/dist/components/icons/chrome-marks.d.ts.map +1 -0
  34. package/dist/components/icons/chrome-marks.js +51 -0
  35. package/dist/components/icons/chrome-marks.js.map +1 -0
  36. package/dist/components/icons/icon-frame.d.ts +29 -0
  37. package/dist/components/icons/icon-frame.d.ts.map +1 -0
  38. package/dist/components/icons/icon-frame.js +19 -0
  39. package/dist/components/icons/icon-frame.js.map +1 -0
  40. package/dist/components/icons/index.d.ts +40 -0
  41. package/dist/components/icons/index.d.ts.map +1 -0
  42. package/dist/components/icons/index.js +6 -0
  43. package/dist/components/icons/index.js.map +1 -0
  44. package/dist/components/icons/nav-map.d.ts +14 -0
  45. package/dist/components/icons/nav-map.d.ts.map +1 -0
  46. package/dist/components/icons/nav-map.js +28 -0
  47. package/dist/components/icons/nav-map.js.map +1 -0
  48. package/dist/components/icons/nav-marks.d.ts +44 -0
  49. package/dist/components/icons/nav-marks.d.ts.map +1 -0
  50. package/dist/components/icons/nav-marks.js +45 -0
  51. package/dist/components/icons/nav-marks.js.map +1 -0
  52. package/dist/components/icons/status-marks.d.ts +21 -0
  53. package/dist/components/icons/status-marks.d.ts.map +1 -0
  54. package/dist/components/icons/status-marks.js +22 -0
  55. package/dist/components/icons/status-marks.js.map +1 -0
  56. package/dist/components/icons/theme-marks.d.ts +9 -0
  57. package/dist/components/icons/theme-marks.d.ts.map +1 -0
  58. package/dist/components/icons/theme-marks.js +10 -0
  59. package/dist/components/icons/theme-marks.js.map +1 -0
  60. package/dist/components/json-diff.d.ts.map +1 -1
  61. package/dist/components/json-diff.js +1 -3
  62. package/dist/components/json-diff.js.map +1 -1
  63. package/dist/components/json-tree-context.d.ts +13 -0
  64. package/dist/components/json-tree-context.d.ts.map +1 -0
  65. package/dist/components/json-tree-context.js +15 -0
  66. package/dist/components/json-tree-context.js.map +1 -0
  67. package/dist/components/json-tree-model.d.ts +56 -0
  68. package/dist/components/json-tree-model.d.ts.map +1 -0
  69. package/dist/components/json-tree-model.js +125 -0
  70. package/dist/components/json-tree-model.js.map +1 -0
  71. package/dist/components/json-tree-nodes.d.ts +30 -0
  72. package/dist/components/json-tree-nodes.d.ts.map +1 -0
  73. package/dist/components/json-tree-nodes.js +118 -0
  74. package/dist/components/json-tree-nodes.js.map +1 -0
  75. package/dist/components/json-tree.d.ts.map +1 -1
  76. package/dist/components/json-tree.js +7 -295
  77. package/dist/components/json-tree.js.map +1 -1
  78. package/dist/components/json-value-format.d.ts +20 -0
  79. package/dist/components/json-value-format.d.ts.map +1 -0
  80. package/dist/components/json-value-format.js +63 -0
  81. package/dist/components/json-value-format.js.map +1 -0
  82. package/dist/components/markdown-blocks.d.ts +38 -0
  83. package/dist/components/markdown-blocks.d.ts.map +1 -0
  84. package/dist/components/markdown-blocks.js +168 -0
  85. package/dist/components/markdown-blocks.js.map +1 -0
  86. package/dist/components/markdown-inline.d.ts +24 -0
  87. package/dist/components/markdown-inline.d.ts.map +1 -0
  88. package/dist/components/markdown-inline.js +257 -0
  89. package/dist/components/markdown-inline.js.map +1 -0
  90. package/dist/components/markdown.d.ts +3 -52
  91. package/dist/components/markdown.d.ts.map +1 -1
  92. package/dist/components/markdown.js +4 -376
  93. package/dist/components/markdown.js.map +1 -1
  94. package/dist/components/observer-registry.d.ts +38 -0
  95. package/dist/components/observer-registry.d.ts.map +1 -0
  96. package/dist/components/observer-registry.js +131 -0
  97. package/dist/components/observer-registry.js.map +1 -0
  98. package/dist/components/overflow-measure.d.ts +38 -0
  99. package/dist/components/overflow-measure.d.ts.map +1 -0
  100. package/dist/components/overflow-measure.js +54 -0
  101. package/dist/components/overflow-measure.js.map +1 -0
  102. package/dist/components/prose-regions.d.ts +81 -0
  103. package/dist/components/prose-regions.d.ts.map +1 -0
  104. package/dist/components/prose-regions.js +128 -0
  105. package/dist/components/prose-regions.js.map +1 -0
  106. package/dist/components/scroll-region.d.ts.map +1 -1
  107. package/dist/components/scroll-region.js +3 -298
  108. package/dist/components/scroll-region.js.map +1 -1
  109. package/dist/components/tab-stop-parse.d.ts +53 -0
  110. package/dist/components/tab-stop-parse.d.ts.map +1 -0
  111. package/dist/components/tab-stop-parse.js +202 -0
  112. package/dist/components/tab-stop-parse.js.map +1 -0
  113. package/dist/components/templated-text-field.d.ts.map +1 -1
  114. package/dist/components/templated-text-field.js +10 -148
  115. package/dist/components/templated-text-field.js.map +1 -1
  116. package/dist/components/templated-text-kwargs-editor.d.ts +24 -0
  117. package/dist/components/templated-text-kwargs-editor.d.ts.map +1 -0
  118. package/dist/components/templated-text-kwargs-editor.js +48 -0
  119. package/dist/components/templated-text-kwargs-editor.js.map +1 -0
  120. package/dist/components/templated-text-kwargs.d.ts +37 -0
  121. package/dist/components/templated-text-kwargs.d.ts.map +1 -0
  122. package/dist/components/templated-text-kwargs.js +66 -0
  123. package/dist/components/templated-text-kwargs.js.map +1 -0
  124. package/dist/components/templated-text-source.d.ts +33 -0
  125. package/dist/components/templated-text-source.d.ts.map +1 -0
  126. package/dist/components/templated-text-source.js +53 -0
  127. package/dist/components/templated-text-source.js.map +1 -0
  128. package/dist/components/tool-picker-parts.d.ts +39 -0
  129. package/dist/components/tool-picker-parts.d.ts.map +1 -0
  130. package/dist/components/tool-picker-parts.js +87 -0
  131. package/dist/components/tool-picker-parts.js.map +1 -0
  132. package/dist/components/tool-picker.d.ts +1 -2
  133. package/dist/components/tool-picker.d.ts.map +1 -1
  134. package/dist/components/tool-picker.js +4 -66
  135. package/dist/components/tool-picker.js.map +1 -1
  136. package/dist/components/version-history-panel.d.ts.map +1 -1
  137. package/dist/components/version-history-panel.js +7 -34
  138. package/dist/components/version-history-panel.js.map +1 -1
  139. package/dist/components/version-history-rows.d.ts +48 -0
  140. package/dist/components/version-history-rows.d.ts.map +1 -0
  141. package/dist/components/version-history-rows.js +51 -0
  142. package/dist/components/version-history-rows.js.map +1 -0
  143. package/dist/guards.d.ts +6 -0
  144. package/dist/guards.d.ts.map +1 -0
  145. package/dist/guards.js +8 -0
  146. package/dist/guards.js.map +1 -0
  147. package/dist/hooks/interactions-connection.d.ts +43 -0
  148. package/dist/hooks/interactions-connection.d.ts.map +1 -0
  149. package/dist/hooks/interactions-connection.js +171 -0
  150. package/dist/hooks/interactions-connection.js.map +1 -0
  151. package/dist/hooks/interactions-stream.d.ts +80 -0
  152. package/dist/hooks/interactions-stream.d.ts.map +1 -0
  153. package/dist/hooks/interactions-stream.js +233 -0
  154. package/dist/hooks/interactions-stream.js.map +1 -0
  155. package/dist/hooks/useApi.d.ts +2409 -2293
  156. package/dist/hooks/useApi.d.ts.map +1 -1
  157. package/dist/hooks/useSse.d.ts +3 -6
  158. package/dist/hooks/useSse.d.ts.map +1 -1
  159. package/dist/hooks/useSse.js +38 -451
  160. package/dist/hooks/useSse.js.map +1 -1
  161. package/dist/navigation/app-link.d.ts +25 -0
  162. package/dist/navigation/app-link.d.ts.map +1 -0
  163. package/dist/navigation/app-link.js +33 -0
  164. package/dist/navigation/app-link.js.map +1 -0
  165. package/dist/navigation/context.d.ts +3 -20
  166. package/dist/navigation/context.d.ts.map +1 -1
  167. package/dist/navigation/context.js +7 -267
  168. package/dist/navigation/context.js.map +1 -1
  169. package/dist/navigation/entry-state.d.ts +23 -0
  170. package/dist/navigation/entry-state.d.ts.map +1 -0
  171. package/dist/navigation/entry-state.js +39 -0
  172. package/dist/navigation/entry-state.js.map +1 -0
  173. package/dist/navigation/guard-registry.d.ts +30 -0
  174. package/dist/navigation/guard-registry.d.ts.map +1 -0
  175. package/dist/navigation/guard-registry.js +46 -0
  176. package/dist/navigation/guard-registry.js.map +1 -0
  177. package/dist/navigation/guarded-navigation.d.ts +24 -0
  178. package/dist/navigation/guarded-navigation.d.ts.map +1 -0
  179. package/dist/navigation/guarded-navigation.js +102 -0
  180. package/dist/navigation/guarded-navigation.js.map +1 -0
  181. package/dist/navigation/index.d.ts +3 -2
  182. package/dist/navigation/index.d.ts.map +1 -1
  183. package/dist/navigation/index.js +2 -1
  184. package/dist/navigation/index.js.map +1 -1
  185. package/dist/navigation/types.d.ts +8 -11
  186. package/dist/navigation/types.d.ts.map +1 -1
  187. package/dist/navigation/use-back-forward-guard.d.ts +5 -0
  188. package/dist/navigation/use-back-forward-guard.d.ts.map +1 -0
  189. package/dist/navigation/use-back-forward-guard.js +82 -0
  190. package/dist/navigation/use-back-forward-guard.js.map +1 -0
  191. package/dist/plugin/registry.d.ts +6 -15
  192. package/dist/plugin/registry.d.ts.map +1 -1
  193. package/dist/plugin/registry.js +83 -75
  194. package/dist/plugin/registry.js.map +1 -1
  195. package/dist/schema-editor/SchemaEditor.d.ts +8 -1
  196. package/dist/schema-editor/SchemaEditor.d.ts.map +1 -1
  197. package/dist/schema-editor/SchemaEditor.js +2 -2
  198. package/dist/schema-editor/SchemaEditor.js.map +1 -1
  199. package/dist/schema-form/SecretRefField.d.ts +2 -3
  200. package/dist/schema-form/SecretRefField.d.ts.map +1 -1
  201. package/dist/schema-form/SecretRefField.js +23 -12
  202. package/dist/schema-form/SecretRefField.js.map +1 -1
  203. package/dist/schema-form/classify.d.ts +1 -122
  204. package/dist/schema-form/classify.d.ts.map +1 -1
  205. package/dist/schema-form/classify.js +174 -237
  206. package/dist/schema-form/classify.js.map +1 -1
  207. package/dist/schema-form/default-value.d.ts.map +1 -1
  208. package/dist/schema-form/default-value.js +42 -19
  209. package/dist/schema-form/default-value.js.map +1 -1
  210. package/dist/schema-form/enum-field.d.ts +1 -1
  211. package/dist/schema-form/enum-field.d.ts.map +1 -1
  212. package/dist/schema-form/field-model.d.ts +121 -0
  213. package/dist/schema-form/field-model.d.ts.map +1 -0
  214. package/dist/schema-form/field-model.js +2 -0
  215. package/dist/schema-form/field-model.js.map +1 -0
  216. package/dist/schema-form/field-node.d.ts.map +1 -1
  217. package/dist/schema-form/field-node.js +25 -14
  218. package/dist/schema-form/field-node.js.map +1 -1
  219. package/dist/schema-form/media-field.d.ts +1 -1
  220. package/dist/schema-form/media-field.d.ts.map +1 -1
  221. package/dist/schema-form/media-field.js +47 -28
  222. package/dist/schema-form/media-field.js.map +1 -1
  223. package/dist/schema-form/string-annotations.d.ts +20 -0
  224. package/dist/schema-form/string-annotations.d.ts.map +1 -0
  225. package/dist/schema-form/string-annotations.js +105 -0
  226. package/dist/schema-form/string-annotations.js.map +1 -0
  227. package/dist/schema-form/string-field.d.ts +1 -1
  228. package/dist/schema-form/string-field.d.ts.map +1 -1
  229. package/dist/schema-form/union-field.d.ts +1 -1
  230. package/dist/schema-form/union-field.d.ts.map +1 -1
  231. package/dist/schema-form/union-field.js.map +1 -1
  232. package/dist/schema-form/union.d.ts +1 -1
  233. package/dist/schema-form/union.d.ts.map +1 -1
  234. package/dist/schema-form/validate.d.ts.map +1 -1
  235. package/dist/schema-form/validate.js +126 -105
  236. package/dist/schema-form/validate.js.map +1 -1
  237. package/dist/state-binding/AdapterMapping.d.ts.map +1 -1
  238. package/dist/state-binding/AdapterMapping.js +0 -0
  239. package/dist/state-binding/AdapterMapping.js.map +1 -1
  240. package/dist/state-binding/StateAttachRow.d.ts.map +1 -1
  241. package/dist/state-binding/StateAttachRow.js +52 -36
  242. package/dist/state-binding/StateAttachRow.js.map +1 -1
  243. package/dist/state-binding/adapter-mapping-row.d.ts +17 -0
  244. package/dist/state-binding/adapter-mapping-row.d.ts.map +1 -0
  245. package/dist/state-binding/adapter-mapping-row.js +87 -0
  246. package/dist/state-binding/adapter-mapping-row.js.map +1 -0
  247. package/dist/structured-output/StructuredOutput.d.ts.map +1 -1
  248. package/dist/structured-output/StructuredOutput.js +1 -3
  249. package/dist/structured-output/StructuredOutput.js.map +1 -1
  250. package/package.json +2 -2
  251. package/src/components/explorer-body.tsx +239 -0
  252. package/src/components/explorer-page-size.ts +39 -0
  253. package/src/components/explorer-tag-filter.tsx +80 -0
  254. package/src/components/explorer-tags.ts +64 -0
  255. package/src/components/explorer-view.tsx +114 -366
  256. package/src/components/extension-combo-builder.tsx +35 -152
  257. package/src/components/extension-combo-parts.tsx +237 -0
  258. package/src/components/fleet-report.tsx +97 -54
  259. package/src/components/icons/chrome-marks.tsx +195 -0
  260. package/src/components/icons/icon-frame.tsx +50 -0
  261. package/src/components/icons/index.ts +84 -0
  262. package/src/components/icons/nav-map.ts +55 -0
  263. package/src/components/icons/nav-marks.tsx +205 -0
  264. package/src/components/icons/status-marks.tsx +46 -0
  265. package/src/components/icons/theme-marks.tsx +33 -0
  266. package/src/components/json-diff.tsx +1 -4
  267. package/src/components/json-tree-context.ts +26 -0
  268. package/src/components/json-tree-model.ts +139 -0
  269. package/src/components/json-tree-nodes.tsx +224 -0
  270. package/src/components/json-tree.tsx +15 -434
  271. package/src/components/json-value-format.ts +64 -0
  272. package/src/components/markdown-blocks.ts +198 -0
  273. package/src/components/markdown-inline.tsx +365 -0
  274. package/src/components/markdown.tsx +5 -471
  275. package/src/components/observer-registry.ts +139 -0
  276. package/src/components/overflow-measure.ts +69 -0
  277. package/src/components/prose-regions.ts +153 -0
  278. package/src/components/scroll-region.tsx +20 -360
  279. package/src/components/tab-stop-parse.ts +217 -0
  280. package/src/components/templated-text-field.tsx +38 -330
  281. package/src/components/templated-text-kwargs-editor.tsx +147 -0
  282. package/src/components/templated-text-kwargs.ts +92 -0
  283. package/src/components/templated-text-source.tsx +226 -0
  284. package/src/components/tool-picker-parts.tsx +184 -0
  285. package/src/components/tool-picker.tsx +22 -114
  286. package/src/components/version-history-panel.tsx +38 -142
  287. package/src/components/version-history-rows.tsx +220 -0
  288. package/src/guards.ts +7 -0
  289. package/src/hooks/interactions-connection.ts +227 -0
  290. package/src/hooks/interactions-stream.ts +274 -0
  291. package/src/hooks/useSse.ts +50 -477
  292. package/src/navigation/app-link.tsx +70 -0
  293. package/src/navigation/context.tsx +11 -330
  294. package/src/navigation/entry-state.ts +42 -0
  295. package/src/navigation/guard-registry.ts +65 -0
  296. package/src/navigation/guarded-navigation.ts +147 -0
  297. package/src/navigation/index.ts +2 -2
  298. package/src/navigation/types.ts +8 -11
  299. package/src/navigation/use-back-forward-guard.ts +90 -0
  300. package/src/plugin/registry.ts +111 -90
  301. package/src/schema-editor/SchemaEditor.tsx +9 -1
  302. package/src/schema-form/SecretRefField.tsx +121 -71
  303. package/src/schema-form/classify.ts +244 -395
  304. package/src/schema-form/default-value.ts +48 -17
  305. package/src/schema-form/enum-field.tsx +1 -1
  306. package/src/schema-form/field-model.ts +133 -0
  307. package/src/schema-form/field-node.tsx +94 -38
  308. package/src/schema-form/media-field.tsx +134 -65
  309. package/src/schema-form/string-annotations.ts +111 -0
  310. package/src/schema-form/string-field.tsx +1 -1
  311. package/src/schema-form/union-field.tsx +2 -1
  312. package/src/schema-form/union.ts +1 -1
  313. package/src/schema-form/validate.ts +184 -103
  314. package/src/state-binding/AdapterMapping.tsx +0 -0
  315. package/src/state-binding/StateAttachRow.tsx +222 -147
  316. package/src/state-binding/adapter-mapping-row.tsx +232 -0
  317. package/src/structured-output/StructuredOutput.tsx +1 -4
  318. package/dist/components/icons.d.ts +0 -160
  319. package/dist/components/icons.d.ts.map +0 -1
  320. package/dist/components/icons.js +0 -160
  321. package/dist/components/icons.js.map +0 -1
  322. package/src/components/icons.tsx +0 -586
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Detect the annotations that opt a (ref-resolved) string schema out of the plain
3
+ * text box: the media-upload annotations (`contentEncoding`/`contentMediaType`/
4
+ * `format: "data-url"`) and the `x-tai42-expression` expression-editor annotation.
5
+ * Both degrade to `undefined` on anything malformed, so an annotated field never
6
+ * renders differently from the plain string it falls back to.
7
+ */
8
+ import { isRecord } from '../guards';
9
+ import type { ExpressionAnnotation, ExpressionAnnotationKey, MediaUpload } from './field-model';
10
+ import type { JsonSchema } from './types';
11
+
12
+ const EXPRESSION_KEYWORD = 'x-tai42-expression';
13
+
14
+ /**
15
+ * Detect the media-upload annotations on a (ref-resolved) string schema. A field
16
+ * opts into the upload control when it carries `contentEncoding: "base64"` plus a
17
+ * `contentMediaType`, OR `format: "data-url"`. Anything else is a plain string.
18
+ */
19
+ export function mediaUpload(schema: JsonSchema): MediaUpload | undefined {
20
+ const mediaType =
21
+ typeof schema.contentMediaType === 'string' ? schema.contentMediaType : undefined;
22
+ const maxBytes =
23
+ typeof schema.contentMaxBytes === 'number' && schema.contentMaxBytes > 0
24
+ ? schema.contentMaxBytes
25
+ : undefined;
26
+ if (schema.format === 'data-url') {
27
+ return { encoding: 'data-url', mediaType, maxBytes };
28
+ }
29
+ if (schema.contentEncoding === 'base64' && mediaType !== undefined) {
30
+ return { encoding: 'base64', mediaType, maxBytes };
31
+ }
32
+ return undefined;
33
+ }
34
+
35
+ /** An optional string member: absent is fine, a non-string is a malformed annotation. */
36
+ type StringOrInvalid =
37
+ { readonly ok: true; readonly value: string | undefined } | { readonly ok: false };
38
+
39
+ function optionalString(value: unknown): StringOrInvalid {
40
+ if (value === undefined) return { ok: true, value: undefined };
41
+ if (typeof value === 'string') return { ok: true, value };
42
+ return { ok: false };
43
+ }
44
+
45
+ /** Parse the optional `keys` array: absent → `undefined`, malformed → `null` (reject). */
46
+ function parseAnnotationKeys(
47
+ value: unknown,
48
+ ): readonly ExpressionAnnotationKey[] | undefined | null {
49
+ if (value === undefined) return undefined;
50
+ if (!Array.isArray(value)) return null;
51
+ const keys: ExpressionAnnotationKey[] = [];
52
+ for (const entry of value as readonly unknown[]) {
53
+ if (!isRecord(entry) || typeof entry.name !== 'string' || typeof entry.gloss !== 'string') {
54
+ return null;
55
+ }
56
+ keys.push({ name: entry.name, gloss: entry.gloss });
57
+ }
58
+ return keys;
59
+ }
60
+
61
+ /** Parse the optional `caveats` array: absent → `undefined`, malformed → `null` (reject). */
62
+ function parseCaveatList(value: unknown): readonly string[] | undefined | null {
63
+ if (value === undefined) return undefined;
64
+ if (!Array.isArray(value)) return null;
65
+ const caveats: string[] = [];
66
+ for (const entry of value as readonly unknown[]) {
67
+ if (typeof entry !== 'string') return null;
68
+ caveats.push(entry);
69
+ }
70
+ return caveats;
71
+ }
72
+
73
+ /**
74
+ * Detect the `x-tai42-expression` annotation on a (ref-resolved) string schema,
75
+ * mirroring {@link mediaUpload}: a well-formed annotation opts the field into the
76
+ * jq expression editor; anything else — absent, non-object, unknown language, or
77
+ * any wrongly-typed member — yields `undefined`, so the field renders EXACTLY as
78
+ * the plain string input it is today. Malformed payloads NEVER throw: a bad
79
+ * annotation is a server-side authoring bug, and degrading to the ordinary text
80
+ * box keeps the form usable while staying byte-identical to the unannotated
81
+ * rendering.
82
+ */
83
+ export function expressionAnnotation(schema: JsonSchema): ExpressionAnnotation | undefined {
84
+ const raw: unknown = schema[EXPRESSION_KEYWORD];
85
+ if (!isRecord(raw)) return undefined;
86
+ // The language gate doubles as the forward-compatibility valve: a future
87
+ // language this client does not understand degrades to a plain string field.
88
+ if (raw.language !== 'jq') return undefined;
89
+
90
+ const label = optionalString(raw.label);
91
+ const blurb = optionalString(raw.blurb);
92
+ const returns = optionalString(raw.returns);
93
+ if (!label.ok || !blurb.ok || !returns.ok) return undefined;
94
+
95
+ const keys = parseAnnotationKeys(raw.keys);
96
+ if (keys === null) return undefined;
97
+ const caveats = parseCaveatList(raw.caveats);
98
+ if (caveats === null) return undefined;
99
+
100
+ // `sample` is any JSON — including `null` — so presence is tracked, not typed.
101
+ return {
102
+ language: 'jq',
103
+ label: label.value,
104
+ blurb: blurb.value,
105
+ keys,
106
+ returns: returns.value,
107
+ caveats,
108
+ hasSample: 'sample' in raw,
109
+ sample: raw.sample,
110
+ };
111
+ }
@@ -26,7 +26,7 @@ import { Field } from '../components/field';
26
26
  import { AlertTriangleIcon } from '../components/icons';
27
27
  import { TextInput, Textarea } from '../components/inputs';
28
28
  import { errorMessage } from '../errors';
29
- import type { ExpressionAnnotation, MediaUpload } from './classify';
29
+ import type { ExpressionAnnotation, MediaUpload } from './field-model';
30
30
  import type { ExpressionFieldComponent, ExpressionInputShape } from './context';
31
31
  import { CompletionProviderContext, ExpressionFieldContext } from './context';
32
32
  import { MediaField } from './media-field';
@@ -11,7 +11,8 @@ import type { ReactNode } from 'react';
11
11
 
12
12
  import { Field } from '../components/field';
13
13
  import { Select } from '../components/select';
14
- import { classifySchema, type FieldModel, type UnionVariant } from './classify';
14
+ import { classifySchema } from './classify';
15
+ import type { FieldModel, UnionVariant } from './field-model';
15
16
  import { skeletonValueForSchema } from './default-value';
16
17
  import { FieldGroup } from './field-group';
17
18
  import { FieldNode } from './field-node';
@@ -6,7 +6,7 @@
6
6
  * active (an unset/`null` value, or nothing matches).
7
7
  */
8
8
  import { resolveRef } from './resolve';
9
- import type { UnionVariant } from './classify';
9
+ import type { UnionVariant } from './field-model';
10
10
  import type { JsonSchema } from './types';
11
11
 
12
12
  function isPlainObject(value: unknown): value is Record<string, unknown> {
@@ -6,6 +6,8 @@
6
6
  * malformed value — a malformed value IS the error it reports.
7
7
  */
8
8
  import { classifySchema } from './classify';
9
+ import type { FieldModel } from './field-model';
10
+ import { isRecord } from '../guards';
9
11
  import { decodedByteSize, effectiveMaxBytes, overCapMessage } from './media';
10
12
  import { scalarLabel } from './resolve';
11
13
  import { activeVariantIndex } from './union';
@@ -21,14 +23,20 @@ export interface ValidateOptions {
21
23
  readonly maxUploadBytes?: number;
22
24
  }
23
25
 
26
+ /** The invariants every per-node validator shares while walking one value tree. */
27
+ interface ValidateCtx {
28
+ /** The document root, for `$ref` resolution during re-classification. */
29
+ readonly root: JsonSchema;
30
+ /** The path-keyed error bag every validator writes into. */
31
+ readonly errors: Record<string, string>;
32
+ /** The host's default media byte cap (a field's own cap still overrides it). */
33
+ readonly maxUploadBytes: number | undefined;
34
+ }
35
+
24
36
  const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
25
37
  const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
26
38
  const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
27
39
 
28
- function isPlainObject(value: unknown): value is Record<string, unknown> {
29
- return typeof value === 'object' && value !== null && !Array.isArray(value);
30
- }
31
-
32
40
  function joinPath(path: string, key: string): string {
33
41
  return path === '' ? key : `${path}.${key}`;
34
42
  }
@@ -58,127 +66,200 @@ function formatError(format: string, value: string): string | undefined {
58
66
  }
59
67
  }
60
68
 
61
- function walk(
62
- schema: JsonSchema,
69
+ type ModelOf<K extends FieldModel['kind']> = Extract<FieldModel, { kind: K }>;
70
+
71
+ // A free-form JSON field: any JSON is accepted, subject only to the container the
72
+ // schema commits to (mirrors the renderer's inline check).
73
+ function validateJson(
74
+ model: ModelOf<'json'>,
75
+ value: unknown,
76
+ path: string,
77
+ ctx: ValidateCtx,
78
+ ): void {
79
+ if (model.jsonType === 'object' && !isRecord(value)) {
80
+ ctx.errors[path] = 'must be a JSON object';
81
+ } else if (model.jsonType === 'array' && !Array.isArray(value)) {
82
+ ctx.errors[path] = 'must be a JSON array';
83
+ }
84
+ }
85
+
86
+ function validateConst(
87
+ model: ModelOf<'const'>,
88
+ value: unknown,
89
+ path: string,
90
+ ctx: ValidateCtx,
91
+ ): void {
92
+ if (value !== model.value) ctx.errors[path] = `must equal ${scalarLabel(model.value)}`;
93
+ }
94
+
95
+ function validateEnum(
96
+ model: ModelOf<'enum'>,
97
+ value: unknown,
98
+ path: string,
99
+ ctx: ValidateCtx,
100
+ ): void {
101
+ const allowed = model.options.some((option) => option.value === value);
102
+ if (!allowed) ctx.errors[path] = 'must be one of the allowed values';
103
+ }
104
+
105
+ function validateString(
106
+ model: ModelOf<'string'>,
107
+ value: unknown,
108
+ path: string,
109
+ ctx: ValidateCtx,
110
+ ): void {
111
+ if (typeof value !== 'string') {
112
+ ctx.errors[path] = 'must be a string';
113
+ return;
114
+ }
115
+ // A media-annotated string carries a byte cap; enforce it on the value's
116
+ // DECODED size so a value that arrived by any route is caught here too. The
117
+ // precedence matches the renderer: field `contentMaxBytes` → host cap →
118
+ // shared default.
119
+ if (model.media !== undefined) {
120
+ const cap = effectiveMaxBytes(model.media.maxBytes, ctx.maxUploadBytes);
121
+ const size = decodedByteSize(value);
122
+ if (size > cap) {
123
+ ctx.errors[path] = overCapMessage('The value', size, cap);
124
+ return;
125
+ }
126
+ }
127
+ if (value.length > 0 && model.format !== undefined) {
128
+ const message = formatError(model.format, value);
129
+ if (message !== undefined) ctx.errors[path] = message;
130
+ }
131
+ }
132
+
133
+ function validateNumber(
134
+ model: ModelOf<'number'>,
63
135
  value: unknown,
64
136
  path: string,
65
- root: JsonSchema,
66
- errors: Record<string, string>,
67
- maxUploadBytes: number | undefined,
137
+ ctx: ValidateCtx,
68
138
  ): void {
69
- const classified = classifySchema(schema, root);
139
+ if (typeof value !== 'number' || Number.isNaN(value)) {
140
+ ctx.errors[path] = model.integer ? 'must be an integer' : 'must be a number';
141
+ return;
142
+ }
143
+ if (model.integer && !Number.isInteger(value)) ctx.errors[path] = 'must be an integer';
144
+ }
145
+
146
+ function validateBoolean(value: unknown, path: string, ctx: ValidateCtx): void {
147
+ if (typeof value !== 'boolean') ctx.errors[path] = 'must be true or false';
148
+ }
149
+
150
+ function validateArray(
151
+ model: ModelOf<'array'>,
152
+ value: unknown,
153
+ path: string,
154
+ ctx: ValidateCtx,
155
+ ): void {
156
+ if (!Array.isArray(value)) {
157
+ ctx.errors[path] = 'must be an array';
158
+ return;
159
+ }
160
+ value.forEach((item, index) => {
161
+ walk(model.items, item, `${path}[${String(index)}]`, ctx);
162
+ });
163
+ }
164
+
165
+ function validateObject(
166
+ model: ModelOf<'object'>,
167
+ value: unknown,
168
+ path: string,
169
+ ctx: ValidateCtx,
170
+ ): void {
171
+ if (!isRecord(value)) {
172
+ ctx.errors[path] = 'must be an object';
173
+ return;
174
+ }
175
+ const propMap = new Map(model.properties);
176
+ for (const [name, propSchema] of model.properties) {
177
+ const childPath = joinPath(path, name);
178
+ const childValue = value[name];
179
+ if (childValue === undefined) {
180
+ if (model.required.has(name)) ctx.errors[childPath] = `"${name}" is required`;
181
+ continue;
182
+ }
183
+ walk(propSchema, childValue, childPath, ctx);
184
+ }
185
+ // Required keys naming a property that somehow isn't declared still count.
186
+ for (const name of model.required) {
187
+ if (!propMap.has(name) && value[name] === undefined) {
188
+ ctx.errors[joinPath(path, name)] = `"${name}" is required`;
189
+ }
190
+ }
191
+ }
192
+
193
+ function validateRecord(
194
+ model: ModelOf<'record'>,
195
+ value: unknown,
196
+ path: string,
197
+ ctx: ValidateCtx,
198
+ ): void {
199
+ if (!isRecord(value)) {
200
+ ctx.errors[path] = 'must be an object';
201
+ return;
202
+ }
203
+ for (const [key, entryValue] of Object.entries(value)) {
204
+ walk(model.values, entryValue, joinPath(path, key), ctx);
205
+ }
206
+ }
207
+
208
+ function validateUnion(
209
+ model: ModelOf<'union'>,
210
+ value: unknown,
211
+ path: string,
212
+ ctx: ValidateCtx,
213
+ ): void {
214
+ const index = activeVariantIndex(value, model.variants, model.discriminator, ctx.root);
215
+ const variant = index === -1 ? undefined : model.variants[index];
216
+ if (variant === undefined) {
217
+ ctx.errors[path] = 'does not match any allowed variant';
218
+ return;
219
+ }
220
+ walk(variant.schema, value, path, ctx);
221
+ }
222
+
223
+ function walk(schema: JsonSchema, value: unknown, path: string, ctx: ValidateCtx): void {
224
+ const classified = classifySchema(schema, ctx.root);
70
225
 
71
226
  if (value === null) {
72
- if (!classified.nullable) errors[path] = 'must not be null';
227
+ if (!classified.nullable) ctx.errors[path] = 'must not be null';
73
228
  return;
74
229
  }
75
230
 
76
231
  const { model } = classified;
77
232
  switch (model.kind) {
78
233
  case 'json':
79
- // A free-form JSON field: any JSON is accepted, subject only to the
80
- // container the schema commits to (mirrors the renderer's inline check).
81
- if (model.jsonType === 'object' && !isPlainObject(value)) {
82
- errors[path] = 'must be a JSON object';
83
- } else if (model.jsonType === 'array' && !Array.isArray(value)) {
84
- errors[path] = 'must be a JSON array';
85
- }
234
+ validateJson(model, value, path, ctx);
86
235
  return;
87
236
  case 'const':
88
- if (value !== model.value) errors[path] = `must equal ${scalarLabel(model.value)}`;
237
+ validateConst(model, value, path, ctx);
89
238
  return;
90
- case 'enum': {
91
- const allowed = model.options.some((option) => option.value === value);
92
- if (!allowed) errors[path] = 'must be one of the allowed values';
239
+ case 'enum':
240
+ validateEnum(model, value, path, ctx);
93
241
  return;
94
- }
95
- case 'string': {
96
- if (typeof value !== 'string') {
97
- errors[path] = 'must be a string';
98
- return;
99
- }
100
- // A media-annotated string carries a byte cap; enforce it on the value's
101
- // DECODED size so a value that arrived by any route is caught here too. The
102
- // precedence matches the renderer: field `contentMaxBytes` → host cap →
103
- // shared default.
104
- if (model.media !== undefined) {
105
- const cap = effectiveMaxBytes(model.media.maxBytes, maxUploadBytes);
106
- const size = decodedByteSize(value);
107
- if (size > cap) {
108
- errors[path] = overCapMessage('The value', size, cap);
109
- return;
110
- }
111
- }
112
- if (value.length > 0 && model.format !== undefined) {
113
- const message = formatError(model.format, value);
114
- if (message !== undefined) errors[path] = message;
115
- }
242
+ case 'string':
243
+ validateString(model, value, path, ctx);
116
244
  return;
117
- }
118
- case 'number': {
119
- if (typeof value !== 'number' || Number.isNaN(value)) {
120
- errors[path] = model.integer ? 'must be an integer' : 'must be a number';
121
- return;
122
- }
123
- if (model.integer && !Number.isInteger(value)) errors[path] = 'must be an integer';
245
+ case 'number':
246
+ validateNumber(model, value, path, ctx);
124
247
  return;
125
- }
126
248
  case 'boolean':
127
- if (typeof value !== 'boolean') errors[path] = 'must be true or false';
249
+ validateBoolean(value, path, ctx);
128
250
  return;
129
- case 'array': {
130
- if (!Array.isArray(value)) {
131
- errors[path] = 'must be an array';
132
- return;
133
- }
134
- value.forEach((item, index) => {
135
- walk(model.items, item, `${path}[${String(index)}]`, root, errors, maxUploadBytes);
136
- });
251
+ case 'array':
252
+ validateArray(model, value, path, ctx);
137
253
  return;
138
- }
139
- case 'object': {
140
- if (!isPlainObject(value)) {
141
- errors[path] = 'must be an object';
142
- return;
143
- }
144
- const propMap = new Map(model.properties);
145
- for (const [name, propSchema] of model.properties) {
146
- const childPath = joinPath(path, name);
147
- const childValue = value[name];
148
- if (childValue === undefined) {
149
- if (model.required.has(name)) errors[childPath] = `"${name}" is required`;
150
- continue;
151
- }
152
- walk(propSchema, childValue, childPath, root, errors, maxUploadBytes);
153
- }
154
- // Required keys naming a property that somehow isn't declared still count.
155
- for (const name of model.required) {
156
- if (!propMap.has(name) && value[name] === undefined) {
157
- errors[joinPath(path, name)] = `"${name}" is required`;
158
- }
159
- }
254
+ case 'object':
255
+ validateObject(model, value, path, ctx);
160
256
  return;
161
- }
162
- case 'record': {
163
- if (!isPlainObject(value)) {
164
- errors[path] = 'must be an object';
165
- return;
166
- }
167
- for (const [key, entryValue] of Object.entries(value)) {
168
- walk(model.values, entryValue, joinPath(path, key), root, errors, maxUploadBytes);
169
- }
257
+ case 'record':
258
+ validateRecord(model, value, path, ctx);
170
259
  return;
171
- }
172
- case 'union': {
173
- const index = activeVariantIndex(value, model.variants, model.discriminator, root);
174
- const variant = index === -1 ? undefined : model.variants[index];
175
- if (variant === undefined) {
176
- errors[path] = 'does not match any allowed variant';
177
- return;
178
- }
179
- walk(variant.schema, value, path, root, errors, maxUploadBytes);
260
+ case 'union':
261
+ validateUnion(model, value, path, ctx);
180
262
  return;
181
- }
182
263
  }
183
264
  }
184
265
 
@@ -194,6 +275,6 @@ export function validateAgainstSchema(
194
275
  options?: ValidateOptions,
195
276
  ): SchemaFormErrors {
196
277
  const errors: Record<string, string> = {};
197
- walk(schema, value, '', schema, errors, options?.maxUploadBytes);
278
+ walk(schema, value, '', { root: schema, errors, maxUploadBytes: options?.maxUploadBytes });
198
279
  return errors;
199
280
  }