@terreno/blocks 57.10.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 (237) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +62 -0
  3. package/biome.jsonc +18 -0
  4. package/bunfig.toml +6 -0
  5. package/dist/asks/errors.d.ts +77 -0
  6. package/dist/asks/errors.js +364 -0
  7. package/dist/asks/files.d.ts +53 -0
  8. package/dist/asks/files.js +171 -0
  9. package/dist/asks/formValues.d.ts +32 -0
  10. package/dist/asks/formValues.js +229 -0
  11. package/dist/asks/headless.d.ts +227 -0
  12. package/dist/asks/headless.js +94 -0
  13. package/dist/asks/jsonSchema.d.ts +6 -0
  14. package/dist/asks/jsonSchema.js +62 -0
  15. package/dist/asks/limits.d.ts +58 -0
  16. package/dist/asks/limits.js +61 -0
  17. package/dist/asks/prompt.d.ts +10 -0
  18. package/dist/asks/prompt.js +144 -0
  19. package/dist/asks/schema.d.ts +826 -0
  20. package/dist/asks/schema.js +864 -0
  21. package/dist/asks/simpleCard.d.ts +85 -0
  22. package/dist/asks/simpleCard.js +279 -0
  23. package/dist/asks/validateInput.d.ts +13 -0
  24. package/dist/asks/validateInput.js +19 -0
  25. package/dist/asks/validateResponse.d.ts +13 -0
  26. package/dist/asks/validateResponse.js +278 -0
  27. package/dist/cli.d.ts +11 -0
  28. package/dist/cli.js +58 -0
  29. package/dist/errors.d.ts +51 -0
  30. package/dist/errors.js +106 -0
  31. package/dist/index.d.ts +31 -0
  32. package/dist/index.js +111 -0
  33. package/dist/jsonSchema.d.ts +5 -0
  34. package/dist/jsonSchema.js +12 -0
  35. package/dist/limits.d.ts +27 -0
  36. package/dist/limits.js +30 -0
  37. package/dist/lint.d.ts +21 -0
  38. package/dist/lint.js +388 -0
  39. package/dist/parse.d.ts +16 -0
  40. package/dist/parse.js +95 -0
  41. package/dist/parsePartial.d.ts +12 -0
  42. package/dist/parsePartial.js +105 -0
  43. package/dist/prompt.d.ts +9 -0
  44. package/dist/prompt.js +95 -0
  45. package/dist/schema.d.ts +199 -0
  46. package/dist/schema.js +277 -0
  47. package/dist/validate.d.ts +17 -0
  48. package/dist/validate.js +331 -0
  49. package/package.json +60 -0
  50. package/schemas/pendingAsksResponse.schema.json +195 -0
  51. package/schemas/simpleCard.schema.json +135 -0
  52. package/schemas/turnRequest.schema.json +108 -0
  53. package/schemas/turnResponse.schema.json +204 -0
  54. package/scripts/writeJsonSchemas.ts +11 -0
  55. package/src/asks/docParity.test.ts +65 -0
  56. package/src/asks/errors.test.ts +250 -0
  57. package/src/asks/errors.ts +449 -0
  58. package/src/asks/files.test.ts +294 -0
  59. package/src/asks/files.ts +216 -0
  60. package/src/asks/fixtures/invalid/choice-blank-text.json +20 -0
  61. package/src/asks/fixtures/invalid/choice-default-not-in-options.json +13 -0
  62. package/src/asks/fixtures/invalid/choice-default-selects-two.json +16 -0
  63. package/src/asks/fixtures/invalid/choice-description-too-long.json +24 -0
  64. package/src/asks/fixtures/invalid/choice-duplicate-ids.json +17 -0
  65. package/src/asks/fixtures/invalid/choice-label-too-long.json +23 -0
  66. package/src/asks/fixtures/invalid/choice-many-bounds-not-whole.json +17 -0
  67. package/src/asks/fixtures/invalid/choice-many-bounds-out-of-range.json +17 -0
  68. package/src/asks/fixtures/invalid/choice-many-default-duplicate.json +17 -0
  69. package/src/asks/fixtures/invalid/choice-many-default-too-many.json +15 -0
  70. package/src/asks/fixtures/invalid/choice-many-max-above-options.json +14 -0
  71. package/src/asks/fixtures/invalid/choice-many-min-above-choices.json +13 -0
  72. package/src/asks/fixtures/invalid/choice-many-min-above-max.json +15 -0
  73. package/src/asks/fixtures/invalid/choice-missing-required.json +14 -0
  74. package/src/asks/fixtures/invalid/choice-mixed-errors.json +19 -0
  75. package/src/asks/fixtures/invalid/choice-not-an-object.json +5 -0
  76. package/src/asks/fixtures/invalid/choice-one-with-many-fields.json +19 -0
  77. package/src/asks/fixtures/invalid/choice-option-id-format.json +35 -0
  78. package/src/asks/fixtures/invalid/choice-options-not-objects.json +12 -0
  79. package/src/asks/fixtures/invalid/choice-other-label-too-long.json +25 -0
  80. package/src/asks/fixtures/invalid/choice-other-label-without-allow-other.json +14 -0
  81. package/src/asks/fixtures/invalid/choice-prompt-empty.json +12 -0
  82. package/src/asks/fixtures/invalid/choice-prompt-too-long.json +23 -0
  83. package/src/asks/fixtures/invalid/choice-select-unknown.json +12 -0
  84. package/src/asks/fixtures/invalid/choice-submit-label-too-long.json +13 -0
  85. package/src/asks/fixtures/invalid/choice-title-too-long.json +24 -0
  86. package/src/asks/fixtures/invalid/choice-too-few-options.json +9 -0
  87. package/src/asks/fixtures/invalid/choice-too-many-options.json +219 -0
  88. package/src/asks/fixtures/invalid/choice-unknown-key-option.json +15 -0
  89. package/src/asks/fixtures/invalid/choice-unknown-key-root.json +13 -0
  90. package/src/asks/fixtures/invalid/choice-wrong-types.json +16 -0
  91. package/src/asks/fixtures/invalid/confirm-emoji-label-too-long.json +13 -0
  92. package/src/asks/fixtures/invalid/confirm-label-too-long.json +14 -0
  93. package/src/asks/fixtures/invalid/confirm-labels-alike-after-trim.json +14 -0
  94. package/src/asks/fixtures/invalid/confirm-labels-alike.json +13 -0
  95. package/src/asks/fixtures/invalid/confirm-missing-prompt-blank-label.json +16 -0
  96. package/src/asks/fixtures/invalid/confirm-unknown-keys.json +23 -0
  97. package/src/asks/fixtures/invalid/confirm-wrong-types.json +18 -0
  98. package/src/asks/fixtures/invalid/files-accept-duplicate.json +13 -0
  99. package/src/asks/fixtures/invalid/files-accept-empty.json +13 -0
  100. package/src/asks/fixtures/invalid/files-accept-unknown.json +13 -0
  101. package/src/asks/fixtures/invalid/files-counts-out-of-range.json +19 -0
  102. package/src/asks/fixtures/invalid/files-min-above-max.json +15 -0
  103. package/src/asks/fixtures/invalid/files-missing-accept-unknown-key.json +17 -0
  104. package/src/asks/fixtures/invalid/form-default-bad-format.json +40 -0
  105. package/src/asks/fixtures/invalid/form-default-invalid-date.json +40 -0
  106. package/src/asks/fixtures/invalid/form-default-not-in-options.json +58 -0
  107. package/src/asks/fixtures/invalid/form-default-out-of-range.json +33 -0
  108. package/src/asks/fixtures/invalid/form-default-text-bounds.json +42 -0
  109. package/src/asks/fixtures/invalid/form-default-wrong-type.json +30 -0
  110. package/src/asks/fixtures/invalid/form-duplicate-field-ids.json +24 -0
  111. package/src/asks/fixtures/invalid/form-field-id-format.json +19 -0
  112. package/src/asks/fixtures/invalid/form-field-text-limits.json +33 -0
  113. package/src/asks/fixtures/invalid/form-keys-of-another-type.json +39 -0
  114. package/src/asks/fixtures/invalid/form-length-above-cap.json +30 -0
  115. package/src/asks/fixtures/invalid/form-min-above-max.json +32 -0
  116. package/src/asks/fixtures/invalid/form-no-fields.json +13 -0
  117. package/src/asks/fixtures/invalid/form-select-options.json +44 -0
  118. package/src/asks/fixtures/invalid/form-too-many-fields.json +59 -0
  119. package/src/asks/fixtures/invalid/form-unknown-field-type.json +27 -0
  120. package/src/asks/fixtures/invalid/markdown-bounds-out-of-range.json +18 -0
  121. package/src/asks/fixtures/invalid/markdown-initial-above-cap.json +13 -0
  122. package/src/asks/fixtures/invalid/markdown-max-above-cap.json +13 -0
  123. package/src/asks/fixtures/invalid/markdown-min-above-cap.json +13 -0
  124. package/src/asks/fixtures/invalid/markdown-min-above-max.json +14 -0
  125. package/src/asks/fixtures/invalid/markdown-placeholder-too-long.json +13 -0
  126. package/src/asks/fixtures/invalid/markdown-wrong-types-unknown-key.json +27 -0
  127. package/src/asks/fixtures/valid/choice-emoji-label-cut.json +32 -0
  128. package/src/asks/fixtures/valid/choice-labels-collide-after-cut.json +21 -0
  129. package/src/asks/fixtures/valid/choice-long-text-cut.json +41 -0
  130. package/src/asks/fixtures/valid/choice-many-default-below-min.json +24 -0
  131. package/src/asks/fixtures/valid/choice-many-every-choice-with-other.json +24 -0
  132. package/src/asks/fixtures/valid/choice-many-no-default.json +21 -0
  133. package/src/asks/fixtures/valid/choice-many-optional-no-buttons.json +20 -0
  134. package/src/asks/fixtures/valid/choice-many-options-default-label-collides.json +23 -0
  135. package/src/asks/fixtures/valid/choice-many-options-no-buttons.json +21 -0
  136. package/src/asks/fixtures/valid/choice-many-options-no-default.json +22 -0
  137. package/src/asks/fixtures/valid/choice-many-options-with-default.json +30 -0
  138. package/src/asks/fixtures/valid/choice-many-required-with-default.json +29 -0
  139. package/src/asks/fixtures/valid/choice-many-single-pick-or-other.json +29 -0
  140. package/src/asks/fixtures/valid/choice-many-toppings-with-default.json +36 -0
  141. package/src/asks/fixtures/valid/choice-max-limits.json +245 -0
  142. package/src/asks/fixtures/valid/choice-plan-with-default.json +42 -0
  143. package/src/asks/fixtures/valid/choice-two-options-no-decline.json +32 -0
  144. package/src/asks/fixtures/valid/choice-two-options.json +32 -0
  145. package/src/asks/fixtures/valid/confirm-allow-decline.json +40 -0
  146. package/src/asks/fixtures/valid/confirm-default-labels.json +36 -0
  147. package/src/asks/fixtures/valid/confirm-destructive-delete.json +41 -0
  148. package/src/asks/fixtures/valid/confirm-emoji-labels.json +38 -0
  149. package/src/asks/fixtures/valid/confirm-long-text-cut.json +40 -0
  150. package/src/asks/fixtures/valid/files-every-type.json +28 -0
  151. package/src/asks/fixtures/valid/files-one-csv-no-decline.json +16 -0
  152. package/src/asks/fixtures/valid/files-receipt-photos.json +24 -0
  153. package/src/asks/fixtures/valid/form-boolean-newsletter.json +30 -0
  154. package/src/asks/fixtures/valid/form-date-due.json +44 -0
  155. package/src/asks/fixtures/valid/form-datetime-meeting.json +44 -0
  156. package/src/asks/fixtures/valid/form-datetime-minutes-offset.json +44 -0
  157. package/src/asks/fixtures/valid/form-eight-fields.json +71 -0
  158. package/src/asks/fixtures/valid/form-email-contact.json +44 -0
  159. package/src/asks/fixtures/valid/form-invoice-details.json +68 -0
  160. package/src/asks/fixtures/valid/form-multiselect-channels.json +64 -0
  161. package/src/asks/fixtures/valid/form-number-budget.json +45 -0
  162. package/src/asks/fixtures/valid/form-phone-callback.json +44 -0
  163. package/src/asks/fixtures/valid/form-select-shipping.json +58 -0
  164. package/src/asks/fixtures/valid/form-text-required-default.json +47 -0
  165. package/src/asks/fixtures/valid/form-textarea-notes.json +23 -0
  166. package/src/asks/fixtures/valid/form-time-reminder.json +44 -0
  167. package/src/asks/fixtures/valid/form-url-website.json +30 -0
  168. package/src/asks/fixtures/valid/markdown-announcement-draft.json +39 -0
  169. package/src/asks/fixtures/valid/markdown-empty-draft.json +34 -0
  170. package/src/asks/fixtures/valid/markdown-initial-blank-below-min.json +24 -0
  171. package/src/asks/fixtures/valid/markdown-initial-over-max.json +24 -0
  172. package/src/asks/fixtures/valid/markdown-long-text-cut.json +37 -0
  173. package/src/asks/fixtures/valid/markdown-no-decline.json +28 -0
  174. package/src/asks/fixtures/valid/markdown-no-initial-with-min.json +25 -0
  175. package/src/asks/formValues.ts +320 -0
  176. package/src/asks/headless.test.ts +146 -0
  177. package/src/asks/headless.ts +130 -0
  178. package/src/asks/jsonSchema.test.ts +171 -0
  179. package/src/asks/jsonSchema.ts +89 -0
  180. package/src/asks/limits.ts +58 -0
  181. package/src/asks/prompt.test.ts +200 -0
  182. package/src/asks/prompt.ts +173 -0
  183. package/src/asks/schema.test.ts +198 -0
  184. package/src/asks/schema.ts +1203 -0
  185. package/src/asks/simpleCard.test.ts +998 -0
  186. package/src/asks/simpleCard.ts +341 -0
  187. package/src/asks/validateInput.test.ts +875 -0
  188. package/src/asks/validateInput.ts +24 -0
  189. package/src/asks/validateResponse.test.ts +740 -0
  190. package/src/asks/validateResponse.ts +341 -0
  191. package/src/blocksDocParity.test.ts +21 -0
  192. package/src/cli.test.ts +134 -0
  193. package/src/cli.ts +65 -0
  194. package/src/display.test.ts +68 -0
  195. package/src/errors.test.ts +29 -0
  196. package/src/errors.ts +125 -0
  197. package/src/fixtures/invalid/column-not-found.yaml +14 -0
  198. package/src/fixtures/invalid/column-type.yaml +14 -0
  199. package/src/fixtures/invalid/dataset-not-found.yaml +7 -0
  200. package/src/fixtures/invalid/dataset-too-large.yaml +20 -0
  201. package/src/fixtures/invalid/depth-exceeded.yaml +10 -0
  202. package/src/fixtures/invalid/duplicate-id.yaml +6 -0
  203. package/src/fixtures/invalid/invalid-enum.yaml +5 -0
  204. package/src/fixtures/invalid/key-order.yaml +4 -0
  205. package/src/fixtures/invalid/missing-required.yaml +3 -0
  206. package/src/fixtures/invalid/not-a-document.txt +1 -0
  207. package/src/fixtures/invalid/row-arity.yaml +11 -0
  208. package/src/fixtures/invalid/select-target.yaml +12 -0
  209. package/src/fixtures/invalid/table-too-wide.yaml +9 -0
  210. package/src/fixtures/invalid/too-many-points.yaml +8 -0
  211. package/src/fixtures/invalid/unknown-key.yaml +5 -0
  212. package/src/fixtures/invalid/unsupported-version.yaml +4 -0
  213. package/src/fixtures/invalid/yaml-anchor.yaml +6 -0
  214. package/src/fixtures/partial/cut-after-divider.yaml +5 -0
  215. package/src/fixtures/valid/actions.yaml +32 -0
  216. package/src/fixtures/valid/chart.yaml +20 -0
  217. package/src/fixtures/valid/layout.yaml +26 -0
  218. package/src/fixtures/valid/prose.yaml +4 -0
  219. package/src/html.test.ts +52 -0
  220. package/src/index.test.ts +114 -0
  221. package/src/index.ts +175 -0
  222. package/src/jsonSchema.test.ts +33 -0
  223. package/src/jsonSchema.ts +11 -0
  224. package/src/limits.ts +27 -0
  225. package/src/lint.test.ts +289 -0
  226. package/src/lint.ts +484 -0
  227. package/src/parse.test.ts +281 -0
  228. package/src/parse.ts +104 -0
  229. package/src/parsePartial.test.ts +53 -0
  230. package/src/parsePartial.ts +116 -0
  231. package/src/prompt.test.ts +24 -0
  232. package/src/prompt.ts +104 -0
  233. package/src/schema.ts +509 -0
  234. package/src/tests/askFixtures.ts +40 -0
  235. package/src/validate.perf.test.ts +43 -0
  236. package/src/validate.ts +369 -0
  237. package/tsconfig.json +18 -0
package/src/schema.ts ADDED
@@ -0,0 +1,509 @@
1
+ import {z} from "zod";
2
+
3
+ import {BLOCK_LIMITS} from "./limits";
4
+
5
+ export const HEADING_SIZES = ["sm", "md", "lg", "xl", "2xl"] as const;
6
+ export const HTML_HEIGHTS = ["sm", "md", "lg"] as const;
7
+ export const CALLOUT_STATUSES = ["info", "warning", "alert"] as const;
8
+ export const BADGE_STATUSES = ["info", "error", "warning", "success", "neutral", "active"] as const;
9
+ export const METRIC_TRENDS = ["up", "down", "flat"] as const;
10
+ export const LAYOUT_BLOCK_TYPES = ["columns", "card"] as const;
11
+
12
+ const visibleText = (maxLength: number): z.ZodString =>
13
+ z
14
+ .string()
15
+ .min(1)
16
+ .max(maxLength)
17
+ .refine((value) => value.trim().length > 0, {
18
+ message: "Must contain visible text, not only whitespace.",
19
+ params: {blockCode: "TOO_SHORT"},
20
+ });
21
+
22
+ const blockIdSchema = z
23
+ .string()
24
+ .regex(/^[a-z][a-z0-9_]{0,63}$/)
25
+ .describe("Optional id. Lowercase letters, digits, and underscores, starting with a letter.");
26
+
27
+ const sharedBlockFields = {
28
+ id: blockIdSchema.optional(),
29
+ };
30
+
31
+ export interface HeadingBlock {
32
+ id?: string;
33
+ size?: (typeof HEADING_SIZES)[number];
34
+ text: string;
35
+ type: "heading";
36
+ }
37
+
38
+ export interface TextBlock {
39
+ id?: string;
40
+ markdown: string;
41
+ type: "text";
42
+ }
43
+
44
+ export interface MetricBlock {
45
+ delta?: string;
46
+ helper?: string;
47
+ id?: string;
48
+ label: string;
49
+ trend?: (typeof METRIC_TRENDS)[number];
50
+ type: "metric";
51
+ value: string;
52
+ }
53
+
54
+ export interface BadgeBlock {
55
+ id?: string;
56
+ status?: (typeof BADGE_STATUSES)[number];
57
+ text: string;
58
+ type: "badge";
59
+ }
60
+
61
+ export interface DividerBlock {
62
+ id?: string;
63
+ type: "divider";
64
+ }
65
+
66
+ export interface ContextBlock {
67
+ id?: string;
68
+ text: string;
69
+ type: "context";
70
+ }
71
+
72
+ export const CHART_KINDS = ["line", "bar", "area", "donut"] as const;
73
+ export const CHART_HEIGHTS = ["sm", "md", "lg"] as const;
74
+ export const DATASET_COLUMN_TYPES = ["string", "number", "date"] as const;
75
+ export const DATASET_GRAINS = ["hour", "day", "week", "month"] as const;
76
+
77
+ export interface DatasetColumn {
78
+ name: string;
79
+ type: (typeof DATASET_COLUMN_TYPES)[number];
80
+ }
81
+
82
+ export interface InlineDataset {
83
+ columns: DatasetColumn[];
84
+ rows: unknown[][];
85
+ source?: "inline";
86
+ }
87
+
88
+ export interface RefDataset {
89
+ grain?: (typeof DATASET_GRAINS)[number];
90
+ id: string;
91
+ limit?: number;
92
+ source: "ref";
93
+ }
94
+
95
+ export type Dataset = InlineDataset | RefDataset;
96
+
97
+ export interface ChartBlock {
98
+ data?: string;
99
+ emptyText?: string;
100
+ height?: (typeof CHART_HEIGHTS)[number];
101
+ id?: string;
102
+ kind: (typeof CHART_KINDS)[number];
103
+ legend?: boolean;
104
+ points?: {label: string; value: number}[];
105
+ title?: string;
106
+ type: "chart";
107
+ x?: string;
108
+ y?: string;
109
+ }
110
+
111
+ export interface TableBlock {
112
+ columns?: string[];
113
+ data: string;
114
+ id?: string;
115
+ title?: string;
116
+ type: "table";
117
+ }
118
+
119
+ export const BUTTON_VARIANTS = ["primary", "secondary", "outline", "ghost", "destructive"] as const;
120
+
121
+ export interface ReplyAction {
122
+ kind: "reply";
123
+ text: string;
124
+ }
125
+
126
+ export interface OpenAction {
127
+ kind: "open";
128
+ route?: string;
129
+ url?: string;
130
+ }
131
+
132
+ export interface SelectAction {
133
+ data: string;
134
+ kind: "select";
135
+ target: string;
136
+ }
137
+
138
+ export interface CallbackAction {
139
+ kind: "callback";
140
+ name: string;
141
+ payload?: Record<string, unknown>;
142
+ }
143
+
144
+ export type BlockAction = ReplyAction | OpenAction | SelectAction | CallbackAction;
145
+
146
+ export interface ButtonElement {
147
+ action: BlockAction;
148
+ iconName?: string;
149
+ id: string;
150
+ text: string;
151
+ type: "button";
152
+ variant?: (typeof BUTTON_VARIANTS)[number];
153
+ }
154
+
155
+ export interface SegmentedElement {
156
+ id: string;
157
+ options: {data: string; label: string}[];
158
+ target: string;
159
+ type: "segmented";
160
+ }
161
+
162
+ export interface ActionsBlock {
163
+ elements: (ButtonElement | SegmentedElement)[];
164
+ id: string;
165
+ type: "actions";
166
+ }
167
+
168
+ export interface HtmlBlock {
169
+ height?: (typeof HTML_HEIGHTS)[number];
170
+ html: string;
171
+ id?: string;
172
+ title?: string;
173
+ type: "html";
174
+ }
175
+
176
+ export interface CalloutBlock {
177
+ id?: string;
178
+ status?: (typeof CALLOUT_STATUSES)[number];
179
+ text: string;
180
+ type: "callout";
181
+ }
182
+
183
+ export interface ImageBlock {
184
+ alt: string;
185
+ id?: string;
186
+ src: string;
187
+ type: "image";
188
+ }
189
+
190
+ export interface DetailsBlock {
191
+ id?: string;
192
+ text: string;
193
+ title: string;
194
+ type: "details";
195
+ }
196
+
197
+ export interface ColumnsBlock {
198
+ children: Block[];
199
+ id?: string;
200
+ type: "columns";
201
+ }
202
+
203
+ export interface CardBlock {
204
+ children: Block[];
205
+ id?: string;
206
+ title?: string;
207
+ type: "card";
208
+ }
209
+
210
+ export type LeafBlock =
211
+ | HeadingBlock
212
+ | TextBlock
213
+ | MetricBlock
214
+ | BadgeBlock
215
+ | DividerBlock
216
+ | ContextBlock
217
+ | ChartBlock
218
+ | TableBlock
219
+ | ActionsBlock
220
+ | HtmlBlock
221
+ | CalloutBlock
222
+ | ImageBlock
223
+ | DetailsBlock;
224
+
225
+ export type Block = LeafBlock | ColumnsBlock | CardBlock;
226
+
227
+ export interface BlocksDocument {
228
+ blocks: Block[];
229
+ datasets?: Record<string, Dataset>;
230
+ v: 1;
231
+ }
232
+
233
+ const headingSchema = z
234
+ .object({
235
+ ...sharedBlockFields,
236
+ size: z.enum(HEADING_SIZES).optional(),
237
+ text: visibleText(BLOCK_LIMITS.headingTextMaxLength),
238
+ type: z.literal("heading"),
239
+ })
240
+ .strict();
241
+
242
+ const textSchema = z
243
+ .object({
244
+ ...sharedBlockFields,
245
+ markdown: visibleText(BLOCK_LIMITS.blockTextMaxLength),
246
+ type: z.literal("text"),
247
+ })
248
+ .strict();
249
+
250
+ const metricSchema = z
251
+ .object({
252
+ ...sharedBlockFields,
253
+ delta: visibleText(BLOCK_LIMITS.metricDeltaMaxLength).optional(),
254
+ helper: visibleText(BLOCK_LIMITS.metricHelperMaxLength).optional(),
255
+ label: visibleText(BLOCK_LIMITS.metricLabelMaxLength),
256
+ trend: z.enum(METRIC_TRENDS).optional(),
257
+ type: z.literal("metric"),
258
+ value: visibleText(BLOCK_LIMITS.metricValueMaxLength),
259
+ })
260
+ .strict();
261
+
262
+ const badgeSchema = z
263
+ .object({
264
+ ...sharedBlockFields,
265
+ status: z.enum(BADGE_STATUSES).optional(),
266
+ text: visibleText(BLOCK_LIMITS.badgeTextMaxLength),
267
+ type: z.literal("badge"),
268
+ })
269
+ .strict();
270
+
271
+ const dividerSchema = z
272
+ .object({
273
+ ...sharedBlockFields,
274
+ type: z.literal("divider"),
275
+ })
276
+ .strict();
277
+
278
+ const contextSchema = z
279
+ .object({
280
+ ...sharedBlockFields,
281
+ text: visibleText(BLOCK_LIMITS.contextTextMaxLength),
282
+ type: z.literal("context"),
283
+ })
284
+ .strict();
285
+
286
+ const chartSchema = z
287
+ .object({
288
+ ...sharedBlockFields,
289
+ data: z.string().min(1).max(64).optional(),
290
+ emptyText: visibleText(BLOCK_LIMITS.contextTextMaxLength).optional(),
291
+ height: z.enum(CHART_HEIGHTS).optional(),
292
+ kind: z.enum(CHART_KINDS),
293
+ legend: z.boolean().optional(),
294
+ points: z
295
+ .array(
296
+ z
297
+ .object({
298
+ label: visibleText(BLOCK_LIMITS.metricLabelMaxLength),
299
+ value: z.number().finite(),
300
+ })
301
+ .strict()
302
+ )
303
+ .max(BLOCK_LIMITS.datasetRowMax)
304
+ .optional(),
305
+ title: visibleText(BLOCK_LIMITS.cardTitleMaxLength).optional(),
306
+ type: z.literal("chart"),
307
+ x: z.string().min(1).max(64).optional(),
308
+ y: z.string().min(1).max(64).optional(),
309
+ })
310
+ .strict();
311
+
312
+ const tableSchema = z
313
+ .object({
314
+ ...sharedBlockFields,
315
+ columns: z.array(z.string().min(1).max(64)).min(1).optional(),
316
+ data: z.string().min(1).max(64),
317
+ title: visibleText(BLOCK_LIMITS.cardTitleMaxLength).optional(),
318
+ type: z.literal("table"),
319
+ })
320
+ .strict();
321
+
322
+ const replyActionSchema = z
323
+ .object({
324
+ kind: z.literal("reply"),
325
+ text: visibleText(BLOCK_LIMITS.blockTextMaxLength),
326
+ })
327
+ .strict();
328
+
329
+ const openActionSchema = z
330
+ .object({
331
+ kind: z.literal("open"),
332
+ route: z.string().min(1).max(200).optional(),
333
+ url: z.string().min(1).max(2_000).optional(),
334
+ })
335
+ .strict();
336
+
337
+ const selectActionSchema = z
338
+ .object({
339
+ data: z.string().min(1).max(64),
340
+ kind: z.literal("select"),
341
+ target: z.string().min(1).max(64),
342
+ })
343
+ .strict();
344
+
345
+ const callbackActionSchema = z
346
+ .object({
347
+ kind: z.literal("callback"),
348
+ name: z.string().regex(/^[a-z][A-Za-z0-9_]{0,63}$/),
349
+ payload: z.record(z.string(), z.unknown()).optional(),
350
+ })
351
+ .strict();
352
+
353
+ const actionSchema = z.discriminatedUnion("kind", [
354
+ replyActionSchema,
355
+ openActionSchema,
356
+ selectActionSchema,
357
+ callbackActionSchema,
358
+ ]);
359
+
360
+ const buttonElementSchema = z
361
+ .object({
362
+ action: actionSchema,
363
+ iconName: z.string().min(1).max(40).optional(),
364
+ id: blockIdSchema,
365
+ text: visibleText(80),
366
+ type: z.literal("button"),
367
+ variant: z.enum(BUTTON_VARIANTS).optional(),
368
+ })
369
+ .strict();
370
+
371
+ const segmentedElementSchema = z
372
+ .object({
373
+ id: blockIdSchema,
374
+ options: z
375
+ .array(
376
+ z
377
+ .object({
378
+ data: z.string().min(1).max(64),
379
+ label: visibleText(40),
380
+ })
381
+ .strict()
382
+ )
383
+ .min(2)
384
+ .max(8),
385
+ target: z.string().min(1).max(64),
386
+ type: z.literal("segmented"),
387
+ })
388
+ .strict();
389
+
390
+ const htmlSchema = z
391
+ .object({
392
+ ...sharedBlockFields,
393
+ height: z.enum(HTML_HEIGHTS).optional(),
394
+ html: z.string().min(1),
395
+ title: visibleText(BLOCK_LIMITS.headingTextMaxLength).optional(),
396
+ type: z.literal("html"),
397
+ })
398
+ .strict();
399
+
400
+ const calloutSchema = z
401
+ .object({
402
+ ...sharedBlockFields,
403
+ status: z.enum(CALLOUT_STATUSES).optional(),
404
+ text: visibleText(BLOCK_LIMITS.blockTextMaxLength),
405
+ type: z.literal("callout"),
406
+ })
407
+ .strict();
408
+
409
+ const imageSchema = z
410
+ .object({
411
+ ...sharedBlockFields,
412
+ alt: visibleText(BLOCK_LIMITS.headingTextMaxLength),
413
+ src: z.string().min(1).max(BLOCK_LIMITS.htmlMaxBytes),
414
+ type: z.literal("image"),
415
+ })
416
+ .strict();
417
+
418
+ const detailsSchema = z
419
+ .object({
420
+ ...sharedBlockFields,
421
+ text: visibleText(BLOCK_LIMITS.blockTextMaxLength),
422
+ title: visibleText(BLOCK_LIMITS.headingTextMaxLength),
423
+ type: z.literal("details"),
424
+ })
425
+ .strict();
426
+
427
+ const actionsSchema = z
428
+ .object({
429
+ elements: z
430
+ .array(z.discriminatedUnion("type", [buttonElementSchema, segmentedElementSchema]))
431
+ .min(1)
432
+ .max(BLOCK_LIMITS.actionElementsMax),
433
+ id: blockIdSchema,
434
+ type: z.literal("actions"),
435
+ })
436
+ .strict();
437
+
438
+ const blockSchema: z.ZodType<Block> = z.lazy(() =>
439
+ z.discriminatedUnion("type", [
440
+ headingSchema,
441
+ textSchema,
442
+ metricSchema,
443
+ badgeSchema,
444
+ dividerSchema,
445
+ contextSchema,
446
+ chartSchema,
447
+ tableSchema,
448
+ actionsSchema,
449
+ htmlSchema,
450
+ calloutSchema,
451
+ imageSchema,
452
+ detailsSchema,
453
+ z
454
+ .object({
455
+ ...sharedBlockFields,
456
+ children: z.array(blockSchema).min(BLOCK_LIMITS.columnsMin).max(BLOCK_LIMITS.columnsMax),
457
+ type: z.literal("columns"),
458
+ })
459
+ .strict(),
460
+ z
461
+ .object({
462
+ ...sharedBlockFields,
463
+ children: z.array(blockSchema).min(1).max(BLOCK_LIMITS.maxBlocks),
464
+ title: visibleText(BLOCK_LIMITS.cardTitleMaxLength).optional(),
465
+ type: z.literal("card"),
466
+ })
467
+ .strict(),
468
+ ])
469
+ );
470
+
471
+ const datasetColumnSchema = z
472
+ .object({
473
+ name: z.string().regex(/^[a-z][a-z0-9_]{0,63}$/),
474
+ type: z.enum(DATASET_COLUMN_TYPES),
475
+ })
476
+ .strict();
477
+
478
+ const inlineDatasetSchema = z
479
+ .object({
480
+ columns: z.array(datasetColumnSchema).min(1),
481
+ rows: z.array(z.array(z.union([z.string(), z.number(), z.null()]))),
482
+ source: z.literal("inline").optional(),
483
+ })
484
+ .strict();
485
+
486
+ const refDatasetSchema = z
487
+ .object({
488
+ grain: z.enum(DATASET_GRAINS).optional(),
489
+ id: z.string().regex(/^[A-Za-z0-9_-]{1,80}$/),
490
+ limit: z.number().int().positive().optional(),
491
+ source: z.literal("ref"),
492
+ })
493
+ .strict();
494
+
495
+ const datasetSchema = z.union([refDatasetSchema, inlineDatasetSchema]);
496
+
497
+ /** Document schema. Semantic dataset and chart checks run in `lintDocument`. */
498
+ export const blocksSchema = z
499
+ .object({
500
+ blocks: z.array(blockSchema).min(1).max(BLOCK_LIMITS.maxBlocks),
501
+ datasets: z.record(z.string().regex(/^[a-z][a-z0-9_]{0,63}$/), datasetSchema).optional(),
502
+ v: z.literal(1),
503
+ })
504
+ .strict();
505
+
506
+ export const wrapAsTextDocument = (text: string): BlocksDocument => ({
507
+ blocks: [{markdown: text, type: "text"}],
508
+ v: 1,
509
+ });
@@ -0,0 +1,40 @@
1
+ import {readdirSync, readFileSync} from "node:fs";
2
+ import {join} from "node:path";
3
+ import type {Ask, AskKind} from "../asks/schema";
4
+ import type {SimpleCard} from "../asks/simpleCard";
5
+
6
+ const FIXTURES_DIR = join(import.meta.dir, "..", "asks", "fixtures");
7
+
8
+ export type ValidAskFixture = Ask & {
9
+ name: string;
10
+ simple: SimpleCard;
11
+ };
12
+
13
+ export interface InvalidAskFixture {
14
+ errors: {code: string; path: string}[];
15
+ input: unknown;
16
+ kind: AskKind;
17
+ name: string;
18
+ }
19
+
20
+ const readFixtures = <T>(folder: "invalid" | "valid"): T[] =>
21
+ readdirSync(join(FIXTURES_DIR, folder))
22
+ .filter((file) => file.endsWith(".json"))
23
+ .sort()
24
+ .map((file) => ({
25
+ ...JSON.parse(readFileSync(join(FIXTURES_DIR, folder, file), "utf8")),
26
+ name: file.replace(/\.json$/, ""),
27
+ }));
28
+
29
+ export const validAskFixtures = (): ValidAskFixture[] => readFixtures<ValidAskFixture>("valid");
30
+
31
+ /** The valid fixtures of one kind, typed as that kind. */
32
+ export const validAskFixturesOf = <Kind extends AskKind>(
33
+ kind: Kind
34
+ ): Extract<ValidAskFixture, {kind: Kind}>[] =>
35
+ validAskFixtures().filter(
36
+ (fixture): fixture is Extract<ValidAskFixture, {kind: Kind}> => fixture.kind === kind
37
+ );
38
+
39
+ export const invalidAskFixtures = (): InvalidAskFixture[] =>
40
+ readFixtures<InvalidAskFixture>("invalid");
@@ -0,0 +1,43 @@
1
+ import {describe, expect, it} from "bun:test";
2
+
3
+ import {validateBlocks} from "./validate";
4
+
5
+ describe("validateBlocks performance", () => {
6
+ it("validates a 50-block, 8-dataset document in under 5 ms median", () => {
7
+ const datasets: Record<
8
+ string,
9
+ {columns: {name: string; type: string}[]; rows: [string, number][]}
10
+ > = {};
11
+ for (let index = 0; index < 8; index += 1) {
12
+ datasets[`set_${index}`] = {
13
+ columns: [
14
+ {name: "label", type: "string"},
15
+ {name: "value", type: "number"},
16
+ ],
17
+ rows: [
18
+ ["a", 1],
19
+ ["b", 2],
20
+ ],
21
+ };
22
+ }
23
+ const blocks = Array.from({length: 50}, (_unused, index) =>
24
+ index === 0
25
+ ? {data: "set_0", kind: "bar", type: "chart", x: "label", y: "value"}
26
+ : {type: "divider"}
27
+ );
28
+ const doc: Record<string, unknown> = {};
29
+ doc.v = 1;
30
+ doc.datasets = datasets;
31
+ doc.blocks = blocks;
32
+ const samples: number[] = [];
33
+ for (let run = 0; run < 11; run += 1) {
34
+ const started = performance.now();
35
+ const validated = validateBlocks(doc);
36
+ samples.push(performance.now() - started);
37
+ expect(validated.ok).toBe(true);
38
+ }
39
+ samples.sort((left, right) => left - right);
40
+ const median = samples[5];
41
+ expect(median).toBeLessThan(5);
42
+ });
43
+ });