@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/index.ts ADDED
@@ -0,0 +1,175 @@
1
+ export type {AskErrorCode, AskValidationError} from "./asks/errors";
2
+ export {ASK_ERROR_CODES} from "./asks/errors";
3
+ export type {AskFileAccept, AskFileMimeType, SniffedFileType} from "./asks/files";
4
+ export {
5
+ ASK_FILE_ACCEPT,
6
+ ASK_FILE_ACCEPT_MIME_TYPES,
7
+ acceptedFileMimeTypes,
8
+ checkAskFileBytes,
9
+ fileNotOwnedError,
10
+ isTextFileMimeType,
11
+ parseAskDataUrl,
12
+ sniffFileBytes,
13
+ } from "./asks/files";
14
+ export type {FormValue} from "./asks/formValues";
15
+ export {formDefaultValues, formTextMaxLength} from "./asks/formValues";
16
+ export type {
17
+ AskResponseWithToolCallId,
18
+ PendingAskListItem,
19
+ PendingAskSummary,
20
+ TurnRequest,
21
+ TurnResult,
22
+ } from "./asks/headless";
23
+ export {
24
+ askResponseWithToolCallIdSchema,
25
+ askSurfaceSchema,
26
+ pendingAskListItemSchema,
27
+ pendingAskListSchema,
28
+ pendingAskSummarySchema,
29
+ turnRequestSchema,
30
+ turnResultSchema,
31
+ } from "./asks/headless";
32
+ export {askJsonSchemas} from "./asks/jsonSchema";
33
+ export {ASK_LIMITS} from "./asks/limits";
34
+ export {askPromptSection} from "./asks/prompt";
35
+ export type {
36
+ Ask,
37
+ AskFileRef,
38
+ AskKind,
39
+ AskResponse,
40
+ AskSurface,
41
+ ChoiceAnswer,
42
+ ChoiceAsk,
43
+ ChoiceAskInput,
44
+ ChoiceAskResponse,
45
+ ChoiceOption,
46
+ ChoiceSelectMode,
47
+ CompactAskKind,
48
+ ConfirmAnswer,
49
+ ConfirmAsk,
50
+ ConfirmAskInput,
51
+ ConfirmAskResponse,
52
+ FilesAnswer,
53
+ FilesAsk,
54
+ FilesAskInput,
55
+ FilesAskResponse,
56
+ FormAnswer,
57
+ FormAsk,
58
+ FormAskInput,
59
+ FormAskResponse,
60
+ FormField,
61
+ FormFieldType,
62
+ MarkdownAnswer,
63
+ MarkdownAsk,
64
+ MarkdownAskInput,
65
+ MarkdownAskResponse,
66
+ } from "./asks/schema";
67
+ export {
68
+ ASK_CANCEL_REASONS,
69
+ ASK_KINDS,
70
+ ASK_SURFACES,
71
+ askAcceptResponseSchema,
72
+ askAllowsDecline,
73
+ askCancelResponseSchema,
74
+ askDeclineResponseSchema,
75
+ askFileRefSchema,
76
+ askInputSchemaFor,
77
+ askInputSchemas,
78
+ askKindsForSurface,
79
+ askOutputSchemas,
80
+ askResponseSchema,
81
+ CHOICE_SELECT_MODES,
82
+ COMPACT_ASK_KINDS,
83
+ choiceAnswerSchema,
84
+ choiceAskInputSchema,
85
+ choiceAskResponseSchema,
86
+ choiceOptionSchema,
87
+ choiceSelectionBounds,
88
+ compactAskInputSchemas,
89
+ compactChoiceAskInputSchema,
90
+ confirmAnswerSchema,
91
+ confirmAskInputSchema,
92
+ confirmAskResponseSchema,
93
+ confirmButtonLabels,
94
+ FORM_FIELD_TYPES,
95
+ filesAnswerSchema,
96
+ filesAskInputSchema,
97
+ filesAskResponseSchema,
98
+ filesCountBounds,
99
+ formAnswerSchema,
100
+ formAskInputSchema,
101
+ formAskResponseSchema,
102
+ formFieldSchema,
103
+ isCompactAskKind,
104
+ markdownAnswerSchema,
105
+ markdownAskInputSchema,
106
+ markdownAskResponseSchema,
107
+ markdownLengthBounds,
108
+ } from "./asks/schema";
109
+ export type {SimpleCard, SimpleCardButton} from "./asks/simpleCard";
110
+ export {
111
+ resolveButtonAnswer,
112
+ SIMPLE_CARD_BUTTON_STYLES,
113
+ simpleCardButtonSchema,
114
+ simpleCardSchema,
115
+ toSimpleCard,
116
+ } from "./asks/simpleCard";
117
+ export {validateAskInput} from "./asks/validateInput";
118
+ export {validateAskResponse} from "./asks/validateResponse";
119
+ export type {BlockError, BlockErrorCode, BlockWarningCode} from "./errors";
120
+ export {BLOCK_ERROR_CODES, BLOCK_WARNING_CODES} from "./errors";
121
+ export {blocksJsonSchema} from "./jsonSchema";
122
+ export {BLOCK_LIMITS} from "./limits";
123
+ export type {KnownDataset, LintBlocksOptions} from "./lint";
124
+ export type {ParseBlocksResult} from "./parse";
125
+ export {parseBlocks} from "./parse";
126
+ export type {ParseBlocksPartialResult} from "./parsePartial";
127
+ export {parseBlocksPartial} from "./parsePartial";
128
+ export {blocksPromptSection} from "./prompt";
129
+ export type {
130
+ ActionsBlock,
131
+ BadgeBlock,
132
+ Block,
133
+ BlockAction,
134
+ BlocksDocument,
135
+ ButtonElement,
136
+ CallbackAction,
137
+ CalloutBlock,
138
+ CardBlock,
139
+ ChartBlock,
140
+ ColumnsBlock,
141
+ ContextBlock,
142
+ Dataset,
143
+ DatasetColumn,
144
+ DetailsBlock,
145
+ DividerBlock,
146
+ HeadingBlock,
147
+ HtmlBlock,
148
+ ImageBlock,
149
+ InlineDataset,
150
+ LeafBlock,
151
+ MetricBlock,
152
+ OpenAction,
153
+ RefDataset,
154
+ ReplyAction,
155
+ SegmentedElement,
156
+ SelectAction,
157
+ TableBlock,
158
+ TextBlock,
159
+ } from "./schema";
160
+ export {
161
+ BADGE_STATUSES,
162
+ BUTTON_VARIANTS,
163
+ blocksSchema,
164
+ CALLOUT_STATUSES,
165
+ CHART_HEIGHTS,
166
+ CHART_KINDS,
167
+ DATASET_COLUMN_TYPES,
168
+ DATASET_GRAINS,
169
+ HEADING_SIZES,
170
+ HTML_HEIGHTS,
171
+ METRIC_TRENDS,
172
+ wrapAsTextDocument,
173
+ } from "./schema";
174
+ export type {ValidateBlocksResult} from "./validate";
175
+ export {validateBlocks} from "./validate";
@@ -0,0 +1,33 @@
1
+ import {describe, expect, it} from "bun:test";
2
+ import {readdirSync, readFileSync} from "node:fs";
3
+ import {join} from "node:path";
4
+ import Ajv from "ajv";
5
+
6
+ import {blocksJsonSchema} from "./jsonSchema";
7
+ import {parseBlocks} from "./parse";
8
+ import {blocksSchema} from "./schema";
9
+
10
+ const fixturesDir = join(import.meta.dir, "fixtures");
11
+
12
+ describe("blocksJsonSchema", () => {
13
+ it("accepts and rejects the same parsed fixtures as the Zod schema", () => {
14
+ const ajv = new Ajv({allErrors: true, strict: false});
15
+ const validate = ajv.compile(blocksJsonSchema);
16
+ const files = [
17
+ ...readdirSync(join(fixturesDir, "valid")).map((name) => join("valid", name)),
18
+ ...readdirSync(join(fixturesDir, "invalid")).map((name) => join("invalid", name)),
19
+ ];
20
+ let compared = 0;
21
+ for (const name of files) {
22
+ const parsed = parseBlocks(readFileSync(join(fixturesDir, name), "utf8"));
23
+ if (!parsed.ok) {
24
+ continue;
25
+ }
26
+ compared += 1;
27
+ const zodOk = blocksSchema.safeParse(parsed.value).success;
28
+ const ajvOk = validate(parsed.value) === true;
29
+ expect(ajvOk).toBe(zodOk);
30
+ }
31
+ expect(compared).toBeGreaterThan(0);
32
+ });
33
+ });
@@ -0,0 +1,11 @@
1
+ import {z} from "zod";
2
+
3
+ import {blocksSchema} from "./schema";
4
+
5
+ /**
6
+ * JSON Schema for the same document `blocksSchema` checks.
7
+ * Semantic lint (dataset columns, select targets, host callbacks) is not expressible here.
8
+ */
9
+ export const blocksJsonSchema: Record<string, unknown> = z.toJSONSchema(blocksSchema, {
10
+ target: "draft-07",
11
+ });
package/src/limits.ts ADDED
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Caps shared by the schema, the prompt, and the reference page.
3
+ */
4
+ export const BLOCK_LIMITS = {
5
+ actionElementsMax: 25,
6
+ badgeTextMaxLength: 80,
7
+ barCategoryWarning: 60,
8
+ blockTextMaxLength: 4_000,
9
+ cardTitleMaxLength: 120,
10
+ columnsMax: 4,
11
+ columnsMin: 2,
12
+ contextTextMaxLength: 280,
13
+ datasetColumnMax: 12,
14
+ datasetRowMax: 500,
15
+ documentTextMaxLength: 20_000,
16
+ donutSliceWarning: 8,
17
+ headingTextMaxLength: 200,
18
+ htmlMaxBytes: 100_000,
19
+ maxBlocks: 50,
20
+ maxDatasets: 8,
21
+ maxDepth: 2,
22
+ metricDeltaMaxLength: 40,
23
+ metricHelperMaxLength: 120,
24
+ metricLabelMaxLength: 80,
25
+ metricValueMaxLength: 80,
26
+ refLimitMax: 1_000,
27
+ } as const;
@@ -0,0 +1,289 @@
1
+ import {describe, expect, it} from "bun:test";
2
+
3
+ import {validateBlocks} from "./validate";
4
+
5
+ const documentWith = ({
6
+ blocks,
7
+ datasets,
8
+ }: {
9
+ blocks: unknown[];
10
+ datasets?: Record<string, unknown>;
11
+ }): Record<string, unknown> => {
12
+ const doc: Record<string, unknown> = {};
13
+ doc.v = 1;
14
+ if (datasets !== undefined) {
15
+ doc.datasets = datasets;
16
+ }
17
+ doc.blocks = blocks;
18
+ return doc;
19
+ };
20
+
21
+ const inline = {
22
+ columns: [
23
+ {name: "month", type: "string"},
24
+ {name: "count", type: "number"},
25
+ ],
26
+ rows: [
27
+ ["Jan", 1],
28
+ ["Feb", 2],
29
+ ],
30
+ };
31
+
32
+ describe("lintDocument", () => {
33
+ it("rejects an element id that matches another id in the document", () => {
34
+ const validated = validateBlocks(
35
+ documentWith({
36
+ blocks: [
37
+ {
38
+ elements: [
39
+ {
40
+ action: {kind: "reply", text: "Again"},
41
+ id: "followups",
42
+ text: "Again",
43
+ type: "button",
44
+ },
45
+ ],
46
+ id: "followups",
47
+ type: "actions",
48
+ },
49
+ ],
50
+ })
51
+ );
52
+ expect(validated.ok).toBe(false);
53
+ if (validated.ok) {
54
+ return;
55
+ }
56
+ expect(validated.errors.map((error) => error.code)).toEqual(["DUPLICATE_ID"]);
57
+ expect(validated.errors[0]?.path).toBe("blocks[0].elements[0].id");
58
+ });
59
+
60
+ it("rejects a callback that is not in the host allowlist", () => {
61
+ const doc = documentWith({
62
+ blocks: [
63
+ {
64
+ elements: [
65
+ {
66
+ action: {kind: "callback", name: "exportDataset"},
67
+ id: "export",
68
+ text: "Export",
69
+ type: "button",
70
+ },
71
+ ],
72
+ id: "followups",
73
+ type: "actions",
74
+ },
75
+ ],
76
+ });
77
+ const allowed = validateBlocks(doc, {hostActions: ["exportDataset"]});
78
+ expect(allowed.ok).toBe(true);
79
+
80
+ const rejected = validateBlocks(doc, {hostActions: ["other"]});
81
+ expect(rejected.ok).toBe(false);
82
+ if (rejected.ok) {
83
+ return;
84
+ }
85
+ expect(rejected.errors.map((error) => error.code)).toEqual(["UNKNOWN_HOST_ACTION"]);
86
+
87
+ const unchecked = validateBlocks(doc);
88
+ expect(unchecked.ok).toBe(true);
89
+ });
90
+
91
+ it("requires an open action to set url or route, not both", () => {
92
+ const open = (action: Record<string, unknown>): Record<string, unknown> =>
93
+ documentWith({
94
+ blocks: [
95
+ {
96
+ elements: [{action, id: "go", text: "Go", type: "button"}],
97
+ id: "followups",
98
+ type: "actions",
99
+ },
100
+ ],
101
+ });
102
+ expect(validateBlocks(open({kind: "open", route: "/reports"})).ok).toBe(true);
103
+ expect(validateBlocks(open({kind: "open", url: "https://example.com"})).ok).toBe(true);
104
+ const neither = validateBlocks(open({kind: "open"}));
105
+ expect(neither.ok).toBe(false);
106
+ if (!neither.ok) {
107
+ expect(neither.errors.map((error) => ({code: error.code, path: error.path}))).toEqual([
108
+ {code: "MISSING_REQUIRED", path: "blocks[0].elements[0].action"},
109
+ ]);
110
+ }
111
+ expect(
112
+ validateBlocks(open({kind: "open", route: "/reports", url: "https://example.com"})).ok
113
+ ).toBe(false);
114
+ });
115
+
116
+ it("warns when a bar, donut, or line chart is a poor fit", () => {
117
+ const rows = Array.from({length: 61}, (_unused, index) => [`m${index}`, index]);
118
+ const bar = validateBlocks(
119
+ documentWith({
120
+ blocks: [{data: "signups", kind: "bar", type: "chart", x: "month", y: "count"}],
121
+ datasets: {signups: {...inline, rows}},
122
+ })
123
+ );
124
+ expect(bar.ok).toBe(true);
125
+ if (!bar.ok) {
126
+ return;
127
+ }
128
+ expect(bar.warnings.map((warning) => warning.code)).toEqual(["BAR_TOO_MANY_CATEGORIES"]);
129
+
130
+ const donut = validateBlocks(
131
+ documentWith({
132
+ blocks: [
133
+ {
134
+ kind: "donut",
135
+ points: Array.from({length: 9}, (_unused, index) => ({
136
+ label: `s${index}`,
137
+ value: index,
138
+ })),
139
+ type: "chart",
140
+ },
141
+ ],
142
+ })
143
+ );
144
+ expect(donut.ok).toBe(true);
145
+ if (!donut.ok) {
146
+ return;
147
+ }
148
+ expect(donut.warnings.map((warning) => warning.code)).toEqual(["DONUT_TOO_MANY_SLICES"]);
149
+
150
+ const line = validateBlocks(
151
+ documentWith({
152
+ blocks: [{data: "signups", kind: "line", type: "chart", x: "month", y: "count"}],
153
+ datasets: {signups: {...inline, rows: [["Jan", 1]]}},
154
+ })
155
+ );
156
+ expect(line.ok).toBe(true);
157
+ if (!line.ok) {
158
+ return;
159
+ }
160
+ expect(line.warnings.map((warning) => warning.code)).toEqual(["LINE_SINGLE_POINT"]);
161
+ });
162
+
163
+ it("checks ref columns only when the handle is known", () => {
164
+ const doc = documentWith({
165
+ blocks: [{data: "daily", kind: "line", type: "chart", x: "day", y: "count"}],
166
+ datasets: {daily: {id: "ds_1", limit: 200, source: "ref"}},
167
+ });
168
+ expect(validateBlocks(doc).ok).toBe(true);
169
+ const checked = validateBlocks(doc, {
170
+ knownDatasets: {
171
+ ds_1: {
172
+ columns: [
173
+ {name: "day", type: "date"},
174
+ {name: "count", type: "string"},
175
+ ],
176
+ },
177
+ },
178
+ });
179
+ expect(checked.ok).toBe(false);
180
+ if (checked.ok) {
181
+ return;
182
+ }
183
+ expect(checked.errors.map((error) => ({code: error.code, path: error.path}))).toEqual([
184
+ {code: "COLUMN_TYPE_MISMATCH", path: "blocks[0].y"},
185
+ ]);
186
+ });
187
+
188
+ it("rejects a number stored in a string column", () => {
189
+ const validated = validateBlocks(
190
+ documentWith({
191
+ blocks: [{type: "divider"}],
192
+ datasets: {signups: {...inline, rows: [[1, 2]]}},
193
+ })
194
+ );
195
+ expect(validated.ok).toBe(false);
196
+ if (validated.ok) {
197
+ return;
198
+ }
199
+ expect(validated.errors.map((error) => error.code)).toEqual(["COLUMN_TYPE_MISMATCH"]);
200
+ });
201
+
202
+ it("rejects more than 500 inline rows", () => {
203
+ const rows = Array.from({length: 501}, (_unused, index) => [`m${index}`, index]);
204
+ const validated = validateBlocks(
205
+ documentWith({
206
+ blocks: [{type: "divider"}],
207
+ datasets: {signups: {...inline, rows}},
208
+ })
209
+ );
210
+ expect(validated.ok).toBe(false);
211
+ if (validated.ok) {
212
+ return;
213
+ }
214
+ expect(validated.errors.map((error) => ({code: error.code, path: error.path}))).toEqual([
215
+ {code: "DATASET_TOO_LARGE", path: "datasets.signups.rows"},
216
+ ]);
217
+ expect(validated.errors[0]?.fix).toContain("500");
218
+ });
219
+
220
+ it("rejects dataset, chart, table, and image mistakes the schema still allows", () => {
221
+ const datasets: Record<string, unknown> = {};
222
+ for (let index = 0; index < 9; index += 1) {
223
+ datasets[`set${index}`] = {
224
+ columns: [
225
+ {name: "month", type: "string"},
226
+ {name: "month", type: "string"},
227
+ ],
228
+ rows: [["Jan", "Jan"]],
229
+ };
230
+ }
231
+ datasets.signups = {
232
+ columns: [
233
+ {name: "n", type: "number"},
234
+ {name: "count", type: "number"},
235
+ ],
236
+ rows: [[1, 2]],
237
+ };
238
+ const validated = validateBlocks(
239
+ documentWith({
240
+ blocks: [
241
+ {alt: "Chart", src: "not a url", type: "image"},
242
+ {
243
+ data: "signups",
244
+ kind: "bar",
245
+ points: [{label: "Jan", value: 1}],
246
+ type: "chart",
247
+ x: "month",
248
+ y: "count",
249
+ },
250
+ {kind: "line", type: "chart"},
251
+ {data: "signups", kind: "bar", type: "chart", x: "n", y: "count"},
252
+ {data: "signups", kind: "bar", type: "chart", x: "n", y: "missing"},
253
+ {
254
+ elements: [
255
+ {
256
+ id: "grain",
257
+ options: [
258
+ {data: "signups", label: "Month"},
259
+ {data: "other", label: "Week"},
260
+ ],
261
+ target: "missing_chart",
262
+ type: "segmented",
263
+ },
264
+ ],
265
+ id: "row",
266
+ type: "actions",
267
+ },
268
+ {data: "missing", type: "table"},
269
+ {columns: ["missing"], data: "signups", type: "table"},
270
+ ],
271
+ datasets,
272
+ })
273
+ );
274
+ expect(validated.ok).toBe(false);
275
+ if (validated.ok) {
276
+ return;
277
+ }
278
+ const codes = new Set(validated.errors.map((error) => error.code));
279
+ expect(codes.has("TOO_MANY")).toBe(true);
280
+ expect(codes.has("DUPLICATE_ID")).toBe(true);
281
+ expect(codes.has("IMAGE_HOST_NOT_ALLOWED")).toBe(true);
282
+ expect(codes.has("INVALID_TYPE")).toBe(true);
283
+ expect(codes.has("MISSING_REQUIRED")).toBe(true);
284
+ expect(codes.has("COLUMN_TYPE_MISMATCH")).toBe(true);
285
+ expect(codes.has("COLUMN_NOT_FOUND")).toBe(true);
286
+ expect(codes.has("SELECT_TARGET_INVALID")).toBe(true);
287
+ expect(codes.has("DATASET_NOT_FOUND")).toBe(true);
288
+ });
289
+ });