@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
@@ -0,0 +1,278 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validateAskResponse = void 0;
4
+ const errors_1 = require("./errors");
5
+ const files_1 = require("./files");
6
+ const formValues_1 = require("./formValues");
7
+ const limits_1 = require("./limits");
8
+ const schema_1 = require("./schema");
9
+ const countText = (count) => (count === 1 ? "1 option" : `${count} options`);
10
+ /** "exactly 2 options", "1 to 3 options", or "at most 3 options". */
11
+ const boundsText = ({ max, min }) => {
12
+ if (min === max) {
13
+ return `exactly ${countText(min)}`;
14
+ }
15
+ if (min === 0) {
16
+ return `at most ${countText(max)}`;
17
+ }
18
+ return `${min} to ${countText(max)}`;
19
+ };
20
+ const selectionCountDraft = ({ count, hasOther, input, }) => {
21
+ const bounds = (0, schema_1.choiceSelectionBounds)(input);
22
+ if (input.select === "one") {
23
+ return {
24
+ code: "SELECTION_COUNT",
25
+ fix: "Send exactly one option id in content.selected.",
26
+ message: `Choose exactly one option; the answer selects ${count}.`,
27
+ segments: ["content", "selected"],
28
+ };
29
+ }
30
+ const otherNote = input.allowOther === true ? " Other counts as one choice." : "";
31
+ return {
32
+ code: "SELECTION_COUNT",
33
+ fix: `Send ${boundsText(bounds)} in content.selected.${otherNote}`,
34
+ message: `Choose ${boundsText(bounds)}; the answer selects ${count}${hasOther ? ", counting Other" : ""}.`,
35
+ segments: ["content", "selected"],
36
+ };
37
+ };
38
+ const validateChoiceAnswer = (input, content) => {
39
+ const parsed = schema_1.choiceAnswerSchema.safeParse(content);
40
+ if (!parsed.success) {
41
+ return (0, errors_1.issuesToAskErrors)({ issues: parsed.error.issues, prefix: ["content"], root: content });
42
+ }
43
+ const { other, selected } = parsed.data;
44
+ const drafts = [];
45
+ const isOtherAllowed = input.allowOther === true;
46
+ if (other !== undefined && !isOtherAllowed) {
47
+ drafts.push({
48
+ code: "OTHER_NOT_ALLOWED",
49
+ fix: "Remove content.other and pick from the options.",
50
+ message: "This ask does not accept an Other answer.",
51
+ segments: ["content", "other"],
52
+ });
53
+ }
54
+ const hasOther = other !== undefined && isOtherAllowed;
55
+ const count = selected.length + (hasOther ? 1 : 0);
56
+ const { max, min } = (0, schema_1.choiceSelectionBounds)(input);
57
+ if (count < min || count > max) {
58
+ drafts.push(selectionCountDraft({ count, hasOther, input }));
59
+ }
60
+ const offeredIds = new Set(input.options.map((option) => option.id));
61
+ const firstIndexById = new Map();
62
+ selected.forEach((id, index) => {
63
+ const firstIndex = firstIndexById.get(id);
64
+ if (firstIndex !== undefined) {
65
+ drafts.push({
66
+ code: "DUPLICATE_ID",
67
+ fix: "List each option id in content.selected once.",
68
+ message: `${(0, errors_1.quoteValue)(id)} is already selected at ${(0, errors_1.formatAskPath)(["content", "selected", firstIndex])}.`,
69
+ segments: ["content", "selected", index],
70
+ });
71
+ return;
72
+ }
73
+ firstIndexById.set(id, index);
74
+ if (offeredIds.has(id)) {
75
+ return;
76
+ }
77
+ drafts.push({
78
+ code: "OPTION_NOT_OFFERED",
79
+ fix: "Use the id of one of the ask's options.",
80
+ message: `${(0, errors_1.quoteValue)(id)} is not one of the offered options.`,
81
+ segments: ["content", "selected", index],
82
+ });
83
+ });
84
+ return drafts;
85
+ };
86
+ const validateConfirmAnswer = (content) => {
87
+ const parsed = schema_1.confirmAnswerSchema.safeParse(content);
88
+ if (parsed.success) {
89
+ return [];
90
+ }
91
+ return (0, errors_1.issuesToAskErrors)({ issues: parsed.error.issues, prefix: ["content"], root: content });
92
+ };
93
+ const characters = (count) => (count === 1 ? "1 character" : `${count} characters`);
94
+ /**
95
+ * The text must fit the ask's bounds, and `changed` must say truthfully whether it differs from
96
+ * the draft, so the agent can trust `changed: false` to mean the user approved the draft as is.
97
+ */
98
+ const markdownTooLong = (length, max) => ({
99
+ code: "TOO_LONG",
100
+ fix: `Shorten content.markdown to ${characters(max)} or fewer.`,
101
+ message: `The text is ${characters(length)}, but this ask allows at most ${max}.`,
102
+ segments: ["content", "markdown"],
103
+ });
104
+ const isMarkdownTooLong = ({ code, segments }) => code === "TOO_LONG" &&
105
+ segments.length === 2 &&
106
+ segments[0] === "content" &&
107
+ segments[1] === "markdown";
108
+ const validateMarkdownAnswer = (input, content) => {
109
+ const { max, min } = (0, schema_1.markdownLengthBounds)(input);
110
+ const parsed = schema_1.markdownAnswerSchema.safeParse(content);
111
+ if (!parsed.success) {
112
+ const sent = typeof content === "object" && content !== null
113
+ ? content.markdown
114
+ : undefined;
115
+ const length = typeof sent === "string" ? sent.length : 0;
116
+ // The schema's 20,000 cap fires before the ask's own maxLength, so the fix names the tighter one.
117
+ return (0, errors_1.issuesToAskErrors)({ issues: parsed.error.issues, prefix: ["content"], root: content }).map((draft) => (isMarkdownTooLong(draft) ? markdownTooLong(length, max) : draft));
118
+ }
119
+ const { changed, markdown } = parsed.data;
120
+ const drafts = [];
121
+ if (markdown.length > max) {
122
+ drafts.push(markdownTooLong(markdown.length, max));
123
+ }
124
+ const visibleLength = markdown.trim().length;
125
+ if (visibleLength < min) {
126
+ drafts.push({
127
+ code: "TOO_SHORT",
128
+ fix: `Write at least ${characters(min)} in content.markdown.`,
129
+ message: `The text is ${characters(visibleLength)}, but this ask needs at least ${min}.`,
130
+ segments: ["content", "markdown"],
131
+ });
132
+ }
133
+ const isChanged = markdown !== (input.initial ?? "");
134
+ if (changed !== isChanged) {
135
+ drafts.push({
136
+ code: "CHANGED_MISMATCH",
137
+ fix: `Set content.changed to ${String(isChanged)}.`,
138
+ message: isChanged
139
+ ? "The text differs from the draft, but changed is false."
140
+ : "The text is the same as the draft, but changed is true.",
141
+ segments: ["content", "changed"],
142
+ });
143
+ }
144
+ return drafts;
145
+ };
146
+ /**
147
+ * Every value must belong to a field, every required field needs a non-blank value, and every
148
+ * value must be one its field accepts. A blank value on an optional field counts as unanswered.
149
+ */
150
+ const validateFormAnswer = (input, content) => {
151
+ const parsed = schema_1.formAnswerEnvelopeSchema.safeParse(content);
152
+ if (!parsed.success) {
153
+ return (0, errors_1.issuesToAskErrors)({ issues: parsed.error.issues, prefix: ["content"], root: content });
154
+ }
155
+ const { values } = parsed.data;
156
+ const fieldIds = new Set(input.fields.map((field) => field.id));
157
+ const drafts = Object.keys(values)
158
+ .filter((id) => !fieldIds.has(id))
159
+ .map((id) => ({
160
+ code: "UNKNOWN_KEY",
161
+ fix: `Remove ${(0, errors_1.quoteValue)(id)} from content.values.`,
162
+ message: `${(0, errors_1.quoteValue)(id)} is not the id of any field in this form.`,
163
+ segments: ["content", "values", id],
164
+ }));
165
+ for (const field of input.fields) {
166
+ const segments = ["content", "values", field.id];
167
+ const value = Object.hasOwn(values, field.id) ? values[field.id] : undefined;
168
+ if (value === undefined || (0, formValues_1.isBlankFormValue)(value)) {
169
+ if (field.required === true) {
170
+ drafts.push({
171
+ code: "REQUIRED_FIELD",
172
+ fix: `Fill in ${(0, errors_1.quoteValue)(field.label)} (content.values.${field.id}).`,
173
+ message: `${(0, errors_1.quoteValue)(field.label)} is required.`,
174
+ segments,
175
+ });
176
+ }
177
+ continue;
178
+ }
179
+ drafts.push(...(0, formValues_1.checkFormValue)({ field, isDefault: false, segments, value }));
180
+ }
181
+ return drafts;
182
+ };
183
+ const fileCountText = (count) => (count === 1 ? "1 file" : `${count} files`);
184
+ const fileBoundsText = ({ max, min }) => min === max ? `exactly ${fileCountText(min)}` : `${min} to ${fileCountText(max)}`;
185
+ /**
186
+ * The count must fit the ask, each declared type must be one `accept` allows, each file must fit
187
+ * the per-file cap, and a data URL's media type must be the declared type. The server then checks
188
+ * the bytes themselves with `checkAskFileBytes`.
189
+ */
190
+ const validateFilesAnswer = (input, content, maxFileSizeBytes) => {
191
+ const parsed = schema_1.filesAnswerSchema.safeParse(content);
192
+ if (!parsed.success) {
193
+ return (0, errors_1.issuesToAskErrors)({ issues: parsed.error.issues, prefix: ["content"], root: content });
194
+ }
195
+ const { files } = parsed.data;
196
+ const drafts = [];
197
+ const bounds = (0, schema_1.filesCountBounds)(input);
198
+ if (files.length < bounds.min || files.length > bounds.max) {
199
+ drafts.push({
200
+ code: "FILE_COUNT",
201
+ fix: `Send ${fileBoundsText(bounds)} in content.files.`,
202
+ message: `This ask takes ${fileBoundsText(bounds)}; the answer has ${files.length}.`,
203
+ segments: ["content", "files"],
204
+ });
205
+ }
206
+ const accepted = (0, files_1.acceptedFileMimeTypes)(input.accept);
207
+ files.forEach((file, index) => {
208
+ const segments = ["content", "files", index];
209
+ if (!accepted.includes(file.mimeType)) {
210
+ drafts.push({
211
+ code: "FILE_TYPE_NOT_ACCEPTED",
212
+ fix: `Send a file of type ${accepted.join(", ")}.`,
213
+ message: `${(0, errors_1.quoteValue)(file.filename)} is ${file.mimeType}, which this ask does not accept.`,
214
+ segments: [...segments, "mimeType"],
215
+ });
216
+ }
217
+ const dataUrl = file.url === undefined ? undefined : (0, files_1.parseAskDataUrl)(file.url);
218
+ const size = dataUrl === undefined ? file.size : (0, files_1.base64ByteLength)(dataUrl.base64);
219
+ if (Math.max(size, file.size) > maxFileSizeBytes) {
220
+ drafts.push((0, files_1.fileTooLargeDraft)({
221
+ maxFileSizeBytes,
222
+ segments: [...segments, "size"],
223
+ size: Math.max(size, file.size),
224
+ }));
225
+ }
226
+ if (dataUrl !== undefined && dataUrl.mediaType !== file.mimeType) {
227
+ drafts.push({
228
+ code: "MIME_MISMATCH",
229
+ fix: `Make the data URL's media type ${file.mimeType}, or declare ${dataUrl.mediaType}.`,
230
+ message: `The data URL holds ${dataUrl.mediaType}, but the file is declared as ${file.mimeType}.`,
231
+ segments: [...segments, "url"],
232
+ });
233
+ }
234
+ });
235
+ return drafts;
236
+ };
237
+ /**
238
+ * Checks a user's answer against the ask it answers. The client runs it before enabling Submit and
239
+ * the server runs it before resuming the turn. Returns no errors when the answer is valid.
240
+ * `maxFileSizeBytes` is the host's per-file cap for `files` answers (10 MB by default).
241
+ */
242
+ const validateAskResponse = ({ input, kind, maxFileSizeBytes = limits_1.ASK_LIMITS.files.defaultMaxFileSizeBytes, response, }) => {
243
+ const ask = { input, kind };
244
+ const envelope = schema_1.askResponseSchema.safeParse(response);
245
+ if (!envelope.success) {
246
+ return (0, errors_1.finalizeAskErrors)((0, errors_1.issuesToAskErrors)({ issues: envelope.error.issues, root: response }));
247
+ }
248
+ const answer = envelope.data;
249
+ if (answer.action === "cancel") {
250
+ return [];
251
+ }
252
+ if (answer.action === "decline") {
253
+ if ((0, schema_1.askAllowsDecline)(ask)) {
254
+ return [];
255
+ }
256
+ return [
257
+ {
258
+ code: "DECLINE_NOT_ALLOWED",
259
+ fix: 'Answer with action "accept".',
260
+ message: "This ask cannot be skipped.",
261
+ path: "action",
262
+ },
263
+ ];
264
+ }
265
+ switch (ask.kind) {
266
+ case "choice":
267
+ return (0, errors_1.finalizeAskErrors)(validateChoiceAnswer(ask.input, answer.content));
268
+ case "confirm":
269
+ return (0, errors_1.finalizeAskErrors)(validateConfirmAnswer(answer.content));
270
+ case "markdown":
271
+ return (0, errors_1.finalizeAskErrors)(validateMarkdownAnswer(ask.input, answer.content));
272
+ case "form":
273
+ return (0, errors_1.finalizeAskErrors)(validateFormAnswer(ask.input, answer.content));
274
+ case "files":
275
+ return (0, errors_1.finalizeAskErrors)(validateFilesAnswer(ask.input, answer.content, maxFileSizeBytes));
276
+ }
277
+ };
278
+ exports.validateAskResponse = validateAskResponse;
package/dist/cli.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env bun
2
+ export declare const runBlocksCli: (argv: readonly string[], input: {
3
+ readFile: (path: string) => string;
4
+ readStdin: () => string;
5
+ }) => {
6
+ code: number;
7
+ stderr: string;
8
+ stdout: string;
9
+ };
10
+ /** Runs validate when the process was started as the `terreno-blocks` bin. */
11
+ export declare const launchBlocksCli: (argv1: string | undefined) => void;
package/dist/cli.js ADDED
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env bun
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.launchBlocksCli = exports.runBlocksCli = void 0;
5
+ const node_fs_1 = require("node:fs");
6
+ const node_path_1 = require("node:path");
7
+ const parse_1 = require("./parse");
8
+ const validate_1 = require("./validate");
9
+ const usage = "Usage: terreno-blocks validate <file | ->\n";
10
+ const formatError = (error) => `${error.path} ${error.code} ${error.message} — ${error.fix}`;
11
+ const runBlocksCli = (argv, input) => {
12
+ const [command, target, extra] = argv;
13
+ if (command !== "validate" || target === undefined || extra !== undefined) {
14
+ return { code: 1, stderr: usage, stdout: "" };
15
+ }
16
+ let text;
17
+ try {
18
+ text = target === "-" ? input.readStdin() : input.readFile(target);
19
+ }
20
+ catch (error) {
21
+ const message = error instanceof Error ? error.message : "Could not read the file.";
22
+ return { code: 1, stderr: `${message}\n`, stdout: "" };
23
+ }
24
+ const parsed = (0, parse_1.parseBlocks)(text);
25
+ if (!parsed.ok) {
26
+ return { code: 1, stderr: "", stdout: `${parsed.errors.map(formatError).join("\n")}\n` };
27
+ }
28
+ const validated = (0, validate_1.validateBlocks)(parsed.value);
29
+ if (!validated.ok) {
30
+ return { code: 1, stderr: "", stdout: `${validated.errors.map(formatError).join("\n")}\n` };
31
+ }
32
+ const warnings = validated.warnings.length === 0 ? "" : `${validated.warnings.map(formatError).join("\n")}\n`;
33
+ return { code: 0, stderr: "", stdout: warnings };
34
+ };
35
+ exports.runBlocksCli = runBlocksCli;
36
+ const main = () => {
37
+ const result = (0, exports.runBlocksCli)(process.argv.slice(2), {
38
+ readFile: (path) => (0, node_fs_1.readFileSync)(path, "utf8"),
39
+ readStdin: () => (0, node_fs_1.readFileSync)(0, "utf8"),
40
+ });
41
+ if (result.stdout !== "") {
42
+ process.stdout.write(result.stdout);
43
+ }
44
+ if (result.stderr !== "") {
45
+ process.stderr.write(result.stderr);
46
+ }
47
+ process.exit(result.code);
48
+ };
49
+ /** Runs validate when the process was started as the `terreno-blocks` bin. */
50
+ const launchBlocksCli = (argv1) => {
51
+ const entry = (0, node_path_1.basename)(argv1 ?? "");
52
+ // npm links the bin as `terreno-blocks`, so argv[1] is that name rather than cli.js.
53
+ if (entry === "cli.ts" || entry === "cli.js" || entry === "terreno-blocks") {
54
+ main();
55
+ }
56
+ };
57
+ exports.launchBlocksCli = launchBlocksCli;
58
+ (0, exports.launchBlocksCli)(process.argv[1]);
@@ -0,0 +1,51 @@
1
+ /** Error codes for whole-reply documents. Warnings never block rendering. */
2
+ export declare const BLOCK_ERROR_CODES: {
3
+ readonly COLUMN_NOT_FOUND: "A chart or table names a column the dataset does not have.";
4
+ readonly COLUMN_TYPE_MISMATCH: "A column value does not match the column type.";
5
+ readonly DATASET_NOT_FOUND: "A chart or table names a dataset the document does not define.";
6
+ readonly DATASET_TOO_LARGE: "A dataset has more rows or columns than allowed.";
7
+ readonly DEPTH_EXCEEDED: "A columns or card block is nested inside another columns or card block.";
8
+ readonly DUPLICATE_ID: "An id or a column name is used more than once.";
9
+ readonly HTML_DISABLED: "An html block is present and this host has not turned HTML on.";
10
+ readonly HTML_TOO_LARGE: "An html block is larger than 100,000 bytes.";
11
+ readonly IMAGE_HOST_NOT_ALLOWED: "An image URL is not a data:image URL, a file: ref, or an https URL on an allowed host.";
12
+ readonly INVALID_ENUM: "A value is not one of the allowed values.";
13
+ readonly INVALID_FORMAT: "A string does not match its required format.";
14
+ readonly INVALID_TYPE: "A value has the wrong type.";
15
+ readonly KEY_ORDER: "Top-level keys are not in the order v, datasets, blocks.";
16
+ readonly MISSING_REQUIRED: "A required field is missing.";
17
+ readonly NOT_A_DOCUMENT: "The reply is not one YAML or JSON mapping with a v field.";
18
+ readonly ROW_ARITY_MISMATCH: "A dataset row does not have one value per column.";
19
+ readonly SELECT_TARGET_INVALID: "A select action names a block that is not a chart or table.";
20
+ readonly TABLE_TOO_WIDE: "A table lists more columns than allowed.";
21
+ readonly TOO_FEW: "A list has fewer items than allowed.";
22
+ readonly TOO_LONG: "A string is longer than allowed.";
23
+ readonly TOO_MANY: "A list has more items than allowed.";
24
+ readonly TOO_MANY_BLOCKS: "The document has more than the maximum number of blocks.";
25
+ readonly TOO_MANY_POINTS: "A ref dataset asks for more rendered rows than allowed.";
26
+ readonly TOO_SHORT: "A string is empty or only whitespace.";
27
+ readonly UNKNOWN_HOST_ACTION: "A callback names a host action that is not registered.";
28
+ readonly UNKNOWN_KEY: "An object has a field that its schema does not define.";
29
+ readonly UNSUPPORTED_VERSION: "The document version is not 1.";
30
+ readonly YAML_FEATURE_DISALLOWED: "The document uses a YAML anchor, alias, or custom tag.";
31
+ };
32
+ /** Chart heuristics. They ride along on a valid document and do not block rendering. */
33
+ export declare const BLOCK_WARNING_CODES: {
34
+ readonly BAR_TOO_MANY_CATEGORIES: "A bar chart has more categories than fit comfortably.";
35
+ readonly DONUT_TOO_MANY_SLICES: "A donut chart has more slices than fit comfortably.";
36
+ readonly LINE_SINGLE_POINT: "A line chart has only one point.";
37
+ };
38
+ export type BlockWarningCode = keyof typeof BLOCK_WARNING_CODES;
39
+ export type BlockErrorCode = keyof typeof BLOCK_ERROR_CODES;
40
+ export interface BlockError {
41
+ /** A key of `BLOCK_ERROR_CODES` or `BLOCK_WARNING_CODES`. */
42
+ code: BlockErrorCode | BlockWarningCode;
43
+ /** One instruction that fixes the error. */
44
+ fix: string;
45
+ /** What is wrong, in one sentence. */
46
+ message: string;
47
+ /** Where the error is, such as `blocks[0].text`. The root is an empty string. */
48
+ path: string;
49
+ }
50
+ export declare const formatBlockPath: (segments: readonly PropertyKey[]) => string;
51
+ export declare const sortBlockErrors: (errors: readonly BlockError[]) => BlockError[];
package/dist/errors.js ADDED
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sortBlockErrors = exports.formatBlockPath = exports.BLOCK_WARNING_CODES = exports.BLOCK_ERROR_CODES = void 0;
4
+ /** Error codes for whole-reply documents. Warnings never block rendering. */
5
+ exports.BLOCK_ERROR_CODES = {
6
+ COLUMN_NOT_FOUND: "A chart or table names a column the dataset does not have.",
7
+ COLUMN_TYPE_MISMATCH: "A column value does not match the column type.",
8
+ DATASET_NOT_FOUND: "A chart or table names a dataset the document does not define.",
9
+ DATASET_TOO_LARGE: "A dataset has more rows or columns than allowed.",
10
+ DEPTH_EXCEEDED: "A columns or card block is nested inside another columns or card block.",
11
+ DUPLICATE_ID: "An id or a column name is used more than once.",
12
+ HTML_DISABLED: "An html block is present and this host has not turned HTML on.",
13
+ HTML_TOO_LARGE: "An html block is larger than 100,000 bytes.",
14
+ IMAGE_HOST_NOT_ALLOWED: "An image URL is not a data:image URL, a file: ref, or an https URL on an allowed host.",
15
+ INVALID_ENUM: "A value is not one of the allowed values.",
16
+ INVALID_FORMAT: "A string does not match its required format.",
17
+ INVALID_TYPE: "A value has the wrong type.",
18
+ KEY_ORDER: "Top-level keys are not in the order v, datasets, blocks.",
19
+ MISSING_REQUIRED: "A required field is missing.",
20
+ NOT_A_DOCUMENT: "The reply is not one YAML or JSON mapping with a v field.",
21
+ ROW_ARITY_MISMATCH: "A dataset row does not have one value per column.",
22
+ SELECT_TARGET_INVALID: "A select action names a block that is not a chart or table.",
23
+ TABLE_TOO_WIDE: "A table lists more columns than allowed.",
24
+ TOO_FEW: "A list has fewer items than allowed.",
25
+ TOO_LONG: "A string is longer than allowed.",
26
+ TOO_MANY: "A list has more items than allowed.",
27
+ TOO_MANY_BLOCKS: "The document has more than the maximum number of blocks.",
28
+ TOO_MANY_POINTS: "A ref dataset asks for more rendered rows than allowed.",
29
+ TOO_SHORT: "A string is empty or only whitespace.",
30
+ UNKNOWN_HOST_ACTION: "A callback names a host action that is not registered.",
31
+ UNKNOWN_KEY: "An object has a field that its schema does not define.",
32
+ UNSUPPORTED_VERSION: "The document version is not 1.",
33
+ YAML_FEATURE_DISALLOWED: "The document uses a YAML anchor, alias, or custom tag.",
34
+ };
35
+ /** Chart heuristics. They ride along on a valid document and do not block rendering. */
36
+ exports.BLOCK_WARNING_CODES = {
37
+ BAR_TOO_MANY_CATEGORIES: "A bar chart has more categories than fit comfortably.",
38
+ DONUT_TOO_MANY_SLICES: "A donut chart has more slices than fit comfortably.",
39
+ LINE_SINGLE_POINT: "A line chart has only one point.",
40
+ };
41
+ const formatBlockPath = (segments) => segments.reduce((path, segment) => {
42
+ if (typeof segment === "number") {
43
+ return `${path}[${segment}]`;
44
+ }
45
+ const key = String(segment);
46
+ if (path === "") {
47
+ return key;
48
+ }
49
+ return `${path}.${key}`;
50
+ }, "");
51
+ exports.formatBlockPath = formatBlockPath;
52
+ const pathIndex = (segment) => {
53
+ const index = Number(segment);
54
+ if (segment !== "" && String(index) === segment) {
55
+ return index;
56
+ }
57
+ return segment;
58
+ };
59
+ /** Splits `blocks[0].text` into comparable pieces so indexes sort numerically. */
60
+ const blockPathSegments = (path) => {
61
+ const parts = [];
62
+ const pattern = /([^[\].]+)|\[(\d+)\]/g;
63
+ for (const match of path.matchAll(pattern)) {
64
+ const index = match[2];
65
+ if (index !== undefined) {
66
+ parts.push(pathIndex(index));
67
+ continue;
68
+ }
69
+ const key = match[1];
70
+ if (key !== undefined) {
71
+ parts.push(key);
72
+ }
73
+ }
74
+ return parts;
75
+ };
76
+ const compareBlockPaths = (left, right) => {
77
+ const leftParts = blockPathSegments(left);
78
+ const rightParts = blockPathSegments(right);
79
+ const length = Math.max(leftParts.length, rightParts.length);
80
+ for (let index = 0; index < length; index += 1) {
81
+ const leftPart = leftParts[index];
82
+ const rightPart = rightParts[index];
83
+ if (leftPart === undefined) {
84
+ return -1;
85
+ }
86
+ if (rightPart === undefined) {
87
+ return 1;
88
+ }
89
+ if (leftPart === rightPart) {
90
+ continue;
91
+ }
92
+ if (typeof leftPart === "number" && typeof rightPart === "number") {
93
+ return leftPart - rightPart;
94
+ }
95
+ return String(leftPart) < String(rightPart) ? -1 : 1;
96
+ }
97
+ return 0;
98
+ };
99
+ const sortBlockErrors = (errors) => [...errors].sort((left, right) => {
100
+ const byPath = compareBlockPaths(left.path, right.path);
101
+ if (byPath !== 0) {
102
+ return byPath;
103
+ }
104
+ return left.code < right.code ? -1 : 1;
105
+ });
106
+ exports.sortBlockErrors = sortBlockErrors;
@@ -0,0 +1,31 @@
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 { ASK_FILE_ACCEPT, ASK_FILE_ACCEPT_MIME_TYPES, acceptedFileMimeTypes, checkAskFileBytes, fileNotOwnedError, isTextFileMimeType, parseAskDataUrl, sniffFileBytes, } from "./asks/files";
5
+ export type { FormValue } from "./asks/formValues";
6
+ export { formDefaultValues, formTextMaxLength } from "./asks/formValues";
7
+ export type { AskResponseWithToolCallId, PendingAskListItem, PendingAskSummary, TurnRequest, TurnResult, } from "./asks/headless";
8
+ export { askResponseWithToolCallIdSchema, askSurfaceSchema, pendingAskListItemSchema, pendingAskListSchema, pendingAskSummarySchema, turnRequestSchema, turnResultSchema, } from "./asks/headless";
9
+ export { askJsonSchemas } from "./asks/jsonSchema";
10
+ export { ASK_LIMITS } from "./asks/limits";
11
+ export { askPromptSection } from "./asks/prompt";
12
+ export type { Ask, AskFileRef, AskKind, AskResponse, AskSurface, ChoiceAnswer, ChoiceAsk, ChoiceAskInput, ChoiceAskResponse, ChoiceOption, ChoiceSelectMode, CompactAskKind, ConfirmAnswer, ConfirmAsk, ConfirmAskInput, ConfirmAskResponse, FilesAnswer, FilesAsk, FilesAskInput, FilesAskResponse, FormAnswer, FormAsk, FormAskInput, FormAskResponse, FormField, FormFieldType, MarkdownAnswer, MarkdownAsk, MarkdownAskInput, MarkdownAskResponse, } from "./asks/schema";
13
+ export { ASK_CANCEL_REASONS, ASK_KINDS, ASK_SURFACES, askAcceptResponseSchema, askAllowsDecline, askCancelResponseSchema, askDeclineResponseSchema, askFileRefSchema, askInputSchemaFor, askInputSchemas, askKindsForSurface, askOutputSchemas, askResponseSchema, CHOICE_SELECT_MODES, COMPACT_ASK_KINDS, choiceAnswerSchema, choiceAskInputSchema, choiceAskResponseSchema, choiceOptionSchema, choiceSelectionBounds, compactAskInputSchemas, compactChoiceAskInputSchema, confirmAnswerSchema, confirmAskInputSchema, confirmAskResponseSchema, confirmButtonLabels, FORM_FIELD_TYPES, filesAnswerSchema, filesAskInputSchema, filesAskResponseSchema, filesCountBounds, formAnswerSchema, formAskInputSchema, formAskResponseSchema, formFieldSchema, isCompactAskKind, markdownAnswerSchema, markdownAskInputSchema, markdownAskResponseSchema, markdownLengthBounds, } from "./asks/schema";
14
+ export type { SimpleCard, SimpleCardButton } from "./asks/simpleCard";
15
+ export { resolveButtonAnswer, SIMPLE_CARD_BUTTON_STYLES, simpleCardButtonSchema, simpleCardSchema, toSimpleCard, } from "./asks/simpleCard";
16
+ export { validateAskInput } from "./asks/validateInput";
17
+ export { validateAskResponse } from "./asks/validateResponse";
18
+ export type { BlockError, BlockErrorCode, BlockWarningCode } from "./errors";
19
+ export { BLOCK_ERROR_CODES, BLOCK_WARNING_CODES } from "./errors";
20
+ export { blocksJsonSchema } from "./jsonSchema";
21
+ export { BLOCK_LIMITS } from "./limits";
22
+ export type { KnownDataset, LintBlocksOptions } from "./lint";
23
+ export type { ParseBlocksResult } from "./parse";
24
+ export { parseBlocks } from "./parse";
25
+ export type { ParseBlocksPartialResult } from "./parsePartial";
26
+ export { parseBlocksPartial } from "./parsePartial";
27
+ export { blocksPromptSection } from "./prompt";
28
+ export type { ActionsBlock, BadgeBlock, Block, BlockAction, BlocksDocument, ButtonElement, CallbackAction, CalloutBlock, CardBlock, ChartBlock, ColumnsBlock, ContextBlock, Dataset, DatasetColumn, DetailsBlock, DividerBlock, HeadingBlock, HtmlBlock, ImageBlock, InlineDataset, LeafBlock, MetricBlock, OpenAction, RefDataset, ReplyAction, SegmentedElement, SelectAction, TableBlock, TextBlock, } from "./schema";
29
+ export { BADGE_STATUSES, BUTTON_VARIANTS, blocksSchema, CALLOUT_STATUSES, CHART_HEIGHTS, CHART_KINDS, DATASET_COLUMN_TYPES, DATASET_GRAINS, HEADING_SIZES, HTML_HEIGHTS, METRIC_TRENDS, wrapAsTextDocument, } from "./schema";
30
+ export type { ValidateBlocksResult } from "./validate";
31
+ export { validateBlocks } from "./validate";