@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.
- package/LICENSE +21 -0
- package/README.md +62 -0
- package/biome.jsonc +18 -0
- package/bunfig.toml +6 -0
- package/dist/asks/errors.d.ts +77 -0
- package/dist/asks/errors.js +364 -0
- package/dist/asks/files.d.ts +53 -0
- package/dist/asks/files.js +171 -0
- package/dist/asks/formValues.d.ts +32 -0
- package/dist/asks/formValues.js +229 -0
- package/dist/asks/headless.d.ts +227 -0
- package/dist/asks/headless.js +94 -0
- package/dist/asks/jsonSchema.d.ts +6 -0
- package/dist/asks/jsonSchema.js +62 -0
- package/dist/asks/limits.d.ts +58 -0
- package/dist/asks/limits.js +61 -0
- package/dist/asks/prompt.d.ts +10 -0
- package/dist/asks/prompt.js +144 -0
- package/dist/asks/schema.d.ts +826 -0
- package/dist/asks/schema.js +864 -0
- package/dist/asks/simpleCard.d.ts +85 -0
- package/dist/asks/simpleCard.js +279 -0
- package/dist/asks/validateInput.d.ts +13 -0
- package/dist/asks/validateInput.js +19 -0
- package/dist/asks/validateResponse.d.ts +13 -0
- package/dist/asks/validateResponse.js +278 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.js +58 -0
- package/dist/errors.d.ts +51 -0
- package/dist/errors.js +106 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +111 -0
- package/dist/jsonSchema.d.ts +5 -0
- package/dist/jsonSchema.js +12 -0
- package/dist/limits.d.ts +27 -0
- package/dist/limits.js +30 -0
- package/dist/lint.d.ts +21 -0
- package/dist/lint.js +388 -0
- package/dist/parse.d.ts +16 -0
- package/dist/parse.js +95 -0
- package/dist/parsePartial.d.ts +12 -0
- package/dist/parsePartial.js +105 -0
- package/dist/prompt.d.ts +9 -0
- package/dist/prompt.js +95 -0
- package/dist/schema.d.ts +199 -0
- package/dist/schema.js +277 -0
- package/dist/validate.d.ts +17 -0
- package/dist/validate.js +331 -0
- package/package.json +60 -0
- package/schemas/pendingAsksResponse.schema.json +195 -0
- package/schemas/simpleCard.schema.json +135 -0
- package/schemas/turnRequest.schema.json +108 -0
- package/schemas/turnResponse.schema.json +204 -0
- package/scripts/writeJsonSchemas.ts +11 -0
- package/src/asks/docParity.test.ts +65 -0
- package/src/asks/errors.test.ts +250 -0
- package/src/asks/errors.ts +449 -0
- package/src/asks/files.test.ts +294 -0
- package/src/asks/files.ts +216 -0
- package/src/asks/fixtures/invalid/choice-blank-text.json +20 -0
- package/src/asks/fixtures/invalid/choice-default-not-in-options.json +13 -0
- package/src/asks/fixtures/invalid/choice-default-selects-two.json +16 -0
- package/src/asks/fixtures/invalid/choice-description-too-long.json +24 -0
- package/src/asks/fixtures/invalid/choice-duplicate-ids.json +17 -0
- package/src/asks/fixtures/invalid/choice-label-too-long.json +23 -0
- package/src/asks/fixtures/invalid/choice-many-bounds-not-whole.json +17 -0
- package/src/asks/fixtures/invalid/choice-many-bounds-out-of-range.json +17 -0
- package/src/asks/fixtures/invalid/choice-many-default-duplicate.json +17 -0
- package/src/asks/fixtures/invalid/choice-many-default-too-many.json +15 -0
- package/src/asks/fixtures/invalid/choice-many-max-above-options.json +14 -0
- package/src/asks/fixtures/invalid/choice-many-min-above-choices.json +13 -0
- package/src/asks/fixtures/invalid/choice-many-min-above-max.json +15 -0
- package/src/asks/fixtures/invalid/choice-missing-required.json +14 -0
- package/src/asks/fixtures/invalid/choice-mixed-errors.json +19 -0
- package/src/asks/fixtures/invalid/choice-not-an-object.json +5 -0
- package/src/asks/fixtures/invalid/choice-one-with-many-fields.json +19 -0
- package/src/asks/fixtures/invalid/choice-option-id-format.json +35 -0
- package/src/asks/fixtures/invalid/choice-options-not-objects.json +12 -0
- package/src/asks/fixtures/invalid/choice-other-label-too-long.json +25 -0
- package/src/asks/fixtures/invalid/choice-other-label-without-allow-other.json +14 -0
- package/src/asks/fixtures/invalid/choice-prompt-empty.json +12 -0
- package/src/asks/fixtures/invalid/choice-prompt-too-long.json +23 -0
- package/src/asks/fixtures/invalid/choice-select-unknown.json +12 -0
- package/src/asks/fixtures/invalid/choice-submit-label-too-long.json +13 -0
- package/src/asks/fixtures/invalid/choice-title-too-long.json +24 -0
- package/src/asks/fixtures/invalid/choice-too-few-options.json +9 -0
- package/src/asks/fixtures/invalid/choice-too-many-options.json +219 -0
- package/src/asks/fixtures/invalid/choice-unknown-key-option.json +15 -0
- package/src/asks/fixtures/invalid/choice-unknown-key-root.json +13 -0
- package/src/asks/fixtures/invalid/choice-wrong-types.json +16 -0
- package/src/asks/fixtures/invalid/confirm-emoji-label-too-long.json +13 -0
- package/src/asks/fixtures/invalid/confirm-label-too-long.json +14 -0
- package/src/asks/fixtures/invalid/confirm-labels-alike-after-trim.json +14 -0
- package/src/asks/fixtures/invalid/confirm-labels-alike.json +13 -0
- package/src/asks/fixtures/invalid/confirm-missing-prompt-blank-label.json +16 -0
- package/src/asks/fixtures/invalid/confirm-unknown-keys.json +23 -0
- package/src/asks/fixtures/invalid/confirm-wrong-types.json +18 -0
- package/src/asks/fixtures/invalid/files-accept-duplicate.json +13 -0
- package/src/asks/fixtures/invalid/files-accept-empty.json +13 -0
- package/src/asks/fixtures/invalid/files-accept-unknown.json +13 -0
- package/src/asks/fixtures/invalid/files-counts-out-of-range.json +19 -0
- package/src/asks/fixtures/invalid/files-min-above-max.json +15 -0
- package/src/asks/fixtures/invalid/files-missing-accept-unknown-key.json +17 -0
- package/src/asks/fixtures/invalid/form-default-bad-format.json +40 -0
- package/src/asks/fixtures/invalid/form-default-invalid-date.json +40 -0
- package/src/asks/fixtures/invalid/form-default-not-in-options.json +58 -0
- package/src/asks/fixtures/invalid/form-default-out-of-range.json +33 -0
- package/src/asks/fixtures/invalid/form-default-text-bounds.json +42 -0
- package/src/asks/fixtures/invalid/form-default-wrong-type.json +30 -0
- package/src/asks/fixtures/invalid/form-duplicate-field-ids.json +24 -0
- package/src/asks/fixtures/invalid/form-field-id-format.json +19 -0
- package/src/asks/fixtures/invalid/form-field-text-limits.json +33 -0
- package/src/asks/fixtures/invalid/form-keys-of-another-type.json +39 -0
- package/src/asks/fixtures/invalid/form-length-above-cap.json +30 -0
- package/src/asks/fixtures/invalid/form-min-above-max.json +32 -0
- package/src/asks/fixtures/invalid/form-no-fields.json +13 -0
- package/src/asks/fixtures/invalid/form-select-options.json +44 -0
- package/src/asks/fixtures/invalid/form-too-many-fields.json +59 -0
- package/src/asks/fixtures/invalid/form-unknown-field-type.json +27 -0
- package/src/asks/fixtures/invalid/markdown-bounds-out-of-range.json +18 -0
- package/src/asks/fixtures/invalid/markdown-initial-above-cap.json +13 -0
- package/src/asks/fixtures/invalid/markdown-max-above-cap.json +13 -0
- package/src/asks/fixtures/invalid/markdown-min-above-cap.json +13 -0
- package/src/asks/fixtures/invalid/markdown-min-above-max.json +14 -0
- package/src/asks/fixtures/invalid/markdown-placeholder-too-long.json +13 -0
- package/src/asks/fixtures/invalid/markdown-wrong-types-unknown-key.json +27 -0
- package/src/asks/fixtures/valid/choice-emoji-label-cut.json +32 -0
- package/src/asks/fixtures/valid/choice-labels-collide-after-cut.json +21 -0
- package/src/asks/fixtures/valid/choice-long-text-cut.json +41 -0
- package/src/asks/fixtures/valid/choice-many-default-below-min.json +24 -0
- package/src/asks/fixtures/valid/choice-many-every-choice-with-other.json +24 -0
- package/src/asks/fixtures/valid/choice-many-no-default.json +21 -0
- package/src/asks/fixtures/valid/choice-many-optional-no-buttons.json +20 -0
- package/src/asks/fixtures/valid/choice-many-options-default-label-collides.json +23 -0
- package/src/asks/fixtures/valid/choice-many-options-no-buttons.json +21 -0
- package/src/asks/fixtures/valid/choice-many-options-no-default.json +22 -0
- package/src/asks/fixtures/valid/choice-many-options-with-default.json +30 -0
- package/src/asks/fixtures/valid/choice-many-required-with-default.json +29 -0
- package/src/asks/fixtures/valid/choice-many-single-pick-or-other.json +29 -0
- package/src/asks/fixtures/valid/choice-many-toppings-with-default.json +36 -0
- package/src/asks/fixtures/valid/choice-max-limits.json +245 -0
- package/src/asks/fixtures/valid/choice-plan-with-default.json +42 -0
- package/src/asks/fixtures/valid/choice-two-options-no-decline.json +32 -0
- package/src/asks/fixtures/valid/choice-two-options.json +32 -0
- package/src/asks/fixtures/valid/confirm-allow-decline.json +40 -0
- package/src/asks/fixtures/valid/confirm-default-labels.json +36 -0
- package/src/asks/fixtures/valid/confirm-destructive-delete.json +41 -0
- package/src/asks/fixtures/valid/confirm-emoji-labels.json +38 -0
- package/src/asks/fixtures/valid/confirm-long-text-cut.json +40 -0
- package/src/asks/fixtures/valid/files-every-type.json +28 -0
- package/src/asks/fixtures/valid/files-one-csv-no-decline.json +16 -0
- package/src/asks/fixtures/valid/files-receipt-photos.json +24 -0
- package/src/asks/fixtures/valid/form-boolean-newsletter.json +30 -0
- package/src/asks/fixtures/valid/form-date-due.json +44 -0
- package/src/asks/fixtures/valid/form-datetime-meeting.json +44 -0
- package/src/asks/fixtures/valid/form-datetime-minutes-offset.json +44 -0
- package/src/asks/fixtures/valid/form-eight-fields.json +71 -0
- package/src/asks/fixtures/valid/form-email-contact.json +44 -0
- package/src/asks/fixtures/valid/form-invoice-details.json +68 -0
- package/src/asks/fixtures/valid/form-multiselect-channels.json +64 -0
- package/src/asks/fixtures/valid/form-number-budget.json +45 -0
- package/src/asks/fixtures/valid/form-phone-callback.json +44 -0
- package/src/asks/fixtures/valid/form-select-shipping.json +58 -0
- package/src/asks/fixtures/valid/form-text-required-default.json +47 -0
- package/src/asks/fixtures/valid/form-textarea-notes.json +23 -0
- package/src/asks/fixtures/valid/form-time-reminder.json +44 -0
- package/src/asks/fixtures/valid/form-url-website.json +30 -0
- package/src/asks/fixtures/valid/markdown-announcement-draft.json +39 -0
- package/src/asks/fixtures/valid/markdown-empty-draft.json +34 -0
- package/src/asks/fixtures/valid/markdown-initial-blank-below-min.json +24 -0
- package/src/asks/fixtures/valid/markdown-initial-over-max.json +24 -0
- package/src/asks/fixtures/valid/markdown-long-text-cut.json +37 -0
- package/src/asks/fixtures/valid/markdown-no-decline.json +28 -0
- package/src/asks/fixtures/valid/markdown-no-initial-with-min.json +25 -0
- package/src/asks/formValues.ts +320 -0
- package/src/asks/headless.test.ts +146 -0
- package/src/asks/headless.ts +130 -0
- package/src/asks/jsonSchema.test.ts +171 -0
- package/src/asks/jsonSchema.ts +89 -0
- package/src/asks/limits.ts +58 -0
- package/src/asks/prompt.test.ts +200 -0
- package/src/asks/prompt.ts +173 -0
- package/src/asks/schema.test.ts +198 -0
- package/src/asks/schema.ts +1203 -0
- package/src/asks/simpleCard.test.ts +998 -0
- package/src/asks/simpleCard.ts +341 -0
- package/src/asks/validateInput.test.ts +875 -0
- package/src/asks/validateInput.ts +24 -0
- package/src/asks/validateResponse.test.ts +740 -0
- package/src/asks/validateResponse.ts +341 -0
- package/src/blocksDocParity.test.ts +21 -0
- package/src/cli.test.ts +134 -0
- package/src/cli.ts +65 -0
- package/src/display.test.ts +68 -0
- package/src/errors.test.ts +29 -0
- package/src/errors.ts +125 -0
- package/src/fixtures/invalid/column-not-found.yaml +14 -0
- package/src/fixtures/invalid/column-type.yaml +14 -0
- package/src/fixtures/invalid/dataset-not-found.yaml +7 -0
- package/src/fixtures/invalid/dataset-too-large.yaml +20 -0
- package/src/fixtures/invalid/depth-exceeded.yaml +10 -0
- package/src/fixtures/invalid/duplicate-id.yaml +6 -0
- package/src/fixtures/invalid/invalid-enum.yaml +5 -0
- package/src/fixtures/invalid/key-order.yaml +4 -0
- package/src/fixtures/invalid/missing-required.yaml +3 -0
- package/src/fixtures/invalid/not-a-document.txt +1 -0
- package/src/fixtures/invalid/row-arity.yaml +11 -0
- package/src/fixtures/invalid/select-target.yaml +12 -0
- package/src/fixtures/invalid/table-too-wide.yaml +9 -0
- package/src/fixtures/invalid/too-many-points.yaml +8 -0
- package/src/fixtures/invalid/unknown-key.yaml +5 -0
- package/src/fixtures/invalid/unsupported-version.yaml +4 -0
- package/src/fixtures/invalid/yaml-anchor.yaml +6 -0
- package/src/fixtures/partial/cut-after-divider.yaml +5 -0
- package/src/fixtures/valid/actions.yaml +32 -0
- package/src/fixtures/valid/chart.yaml +20 -0
- package/src/fixtures/valid/layout.yaml +26 -0
- package/src/fixtures/valid/prose.yaml +4 -0
- package/src/html.test.ts +52 -0
- package/src/index.test.ts +114 -0
- package/src/index.ts +175 -0
- package/src/jsonSchema.test.ts +33 -0
- package/src/jsonSchema.ts +11 -0
- package/src/limits.ts +27 -0
- package/src/lint.test.ts +289 -0
- package/src/lint.ts +484 -0
- package/src/parse.test.ts +281 -0
- package/src/parse.ts +104 -0
- package/src/parsePartial.test.ts +53 -0
- package/src/parsePartial.ts +116 -0
- package/src/prompt.test.ts +24 -0
- package/src/prompt.ts +104 -0
- package/src/schema.ts +509 -0
- package/src/tests/askFixtures.ts +40 -0
- package/src/validate.perf.test.ts +43 -0
- package/src/validate.ts +369 -0
- package/tsconfig.json +18 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Flourish Health
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# @terreno/blocks
|
|
2
|
+
|
|
3
|
+
Shared contracts for agent-driven UI — ask schemas, answer validation, and simple cards.
|
|
4
|
+
|
|
5
|
+
Runtime-agnostic Zod contracts shared by `@terreno/ai` (server) and `@terreno/ui` (renderer), plus
|
|
6
|
+
JSON Schema documents and fixtures for native clients such as a Swift watch app. The only runtime
|
|
7
|
+
dependency is `zod`.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bun add @terreno/blocks
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Most apps get it through `@terreno/ai` and `@terreno/ui`. Install it directly to validate asks or
|
|
16
|
+
answers in your own code, or to build a client that answers asks without React.
|
|
17
|
+
|
|
18
|
+
## Quick start
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
import {type ChoiceAsk, toSimpleCard, validateAskInput, validateAskResponse} from "@terreno/blocks";
|
|
22
|
+
|
|
23
|
+
const ask: ChoiceAsk = {
|
|
24
|
+
input: {
|
|
25
|
+
options: [
|
|
26
|
+
{id: "starter", label: "Starter"},
|
|
27
|
+
{id: "team", label: "Team"},
|
|
28
|
+
],
|
|
29
|
+
prompt: "Which plan should I set up?",
|
|
30
|
+
select: "one",
|
|
31
|
+
},
|
|
32
|
+
kind: "choice",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const inputErrors = validateAskInput(ask); // [] when the ask is valid
|
|
36
|
+
const answerErrors = validateAskResponse({
|
|
37
|
+
...ask,
|
|
38
|
+
response: {action: "accept", content: {selected: ["team"]}},
|
|
39
|
+
});
|
|
40
|
+
const card = toSimpleCard({...ask, toolCallId: "call_1"}); // title, text, and up to 3 buttons
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## What's included
|
|
44
|
+
|
|
45
|
+
- Ask schemas for every kind (`choice`, `confirm`, `markdown`, `form`, `files`) and the answer
|
|
46
|
+
envelope (`accept`, `decline`, `cancel`)
|
|
47
|
+
- `validateAskInput` and `validateAskResponse`, with stable error codes (`ASK_ERROR_CODES`) and
|
|
48
|
+
limits (`ASK_LIMITS`)
|
|
49
|
+
- Simple cards for small screens: `toSimpleCard`, `resolveButtonAnswer`, and the compact surface
|
|
50
|
+
- Bodies of the headless `pendingAsks` and `turn` endpoints
|
|
51
|
+
- JSON Schema documents (`@terreno/blocks/schemas/*`) and valid and invalid fixtures
|
|
52
|
+
(`@terreno/blocks/fixtures/*`)
|
|
53
|
+
|
|
54
|
+
## Documentation
|
|
55
|
+
|
|
56
|
+
- Reference: [docs/reference/agent-ui-asks.md](https://github.com/TerrenoLabs/terreno/blob/master/docs/reference/agent-ui-asks.md)
|
|
57
|
+
- How-to: [docs/how-to/agent-ui-asks.md](https://github.com/TerrenoLabs/terreno/blob/master/docs/how-to/agent-ui-asks.md)
|
|
58
|
+
- Explanation: [docs/explanation/agent-ui-asks.md](https://github.com/TerrenoLabs/terreno/blob/master/docs/explanation/agent-ui-asks.md)
|
|
59
|
+
|
|
60
|
+
## License and Contributing
|
|
61
|
+
|
|
62
|
+
Licensed under the [MIT License](https://github.com/TerrenoLabs/terreno/blob/master/LICENSE). See [CONTRIBUTING.md](https://github.com/TerrenoLabs/terreno/blob/master/CONTRIBUTING.md) for contribution guidelines.
|
package/biome.jsonc
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
|
|
3
|
+
"extends": ["../biome.jsonc"],
|
|
4
|
+
"files": {
|
|
5
|
+
"includes": [
|
|
6
|
+
"package.json",
|
|
7
|
+
"src/**/*.ts",
|
|
8
|
+
"scripts/**/*.ts",
|
|
9
|
+
"!!**/node_modules",
|
|
10
|
+
"!!node_modules",
|
|
11
|
+
"!!**/dist",
|
|
12
|
+
"!!dist",
|
|
13
|
+
"!!**/coverage",
|
|
14
|
+
"!!coverage"
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
"root": true
|
|
18
|
+
}
|
package/bunfig.toml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
[test]
|
|
2
|
+
root = "./src"
|
|
3
|
+
# `scripts/` holds dev-only generators (`bun run schemas`). `src/asks/jsonSchema.test.ts` fails
|
|
4
|
+
# when their committed output is stale, and the new-file coverage gate reads this list.
|
|
5
|
+
coveragePathIgnorePatterns = ["**/dist/**", "**/*.test.ts", "**/tests/**", "**/node_modules/**", "../**", "scripts/**"]
|
|
6
|
+
coverageThreshold = { line = 95, function = 95 }
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Every ask error code, with its meaning. `validateAskInput` and `validateAskResponse` return all
|
|
4
|
+
* of them except `UNKNOWN_BUTTON`, which the headless `turn` endpoint returns, and
|
|
5
|
+
* `FILE_NOT_OWNED`, which the server returns when it loads an uploaded file.
|
|
6
|
+
*/
|
|
7
|
+
export declare const ASK_ERROR_CODES: {
|
|
8
|
+
readonly CHANGED_MISMATCH: "A markdown answer's changed flag does not match whether its text differs from the draft.";
|
|
9
|
+
readonly DECLINE_NOT_ALLOWED: "The answer skips an ask that does not allow skipping.";
|
|
10
|
+
readonly DEFAULT_NOT_IN_OPTIONS: "A default names an option id that the ask does not offer.";
|
|
11
|
+
readonly DUPLICATE_ID: "An id appears twice where ids must be unique: options, default, accept, or an answer.";
|
|
12
|
+
readonly DUPLICATE_LABEL: "Two buttons would share a label: options of a compact ask, or a confirm's approve and deny.";
|
|
13
|
+
readonly FIELD_TYPE_MISMATCH: "A form value or default does not fit its field's type: the wrong JSON type, not a whole number, or not a valid email, URL, or phone number.";
|
|
14
|
+
readonly FILE_COUNT: "A files answer has fewer files than minFiles or more than maxFiles.";
|
|
15
|
+
readonly FILE_NOT_OWNED: "A files answer names a fileId that is not an upload of the caller, or the host has no file storage.";
|
|
16
|
+
readonly FILE_TOO_LARGE: "A file is larger than the host's per-file upload cap.";
|
|
17
|
+
readonly FILE_TYPE_NOT_ACCEPTED: "A file's declared type is not one the ask's accept list allows.";
|
|
18
|
+
readonly INVALID_DATE: "A form date, time, or datetime value or default is not a real ISO 8601 value in the field's format.";
|
|
19
|
+
readonly INVALID_ENUM: "A value is not one of the allowed values.";
|
|
20
|
+
readonly INVALID_FORMAT: "A string does not match its required format.";
|
|
21
|
+
readonly INVALID_TYPE: "A value has the wrong type.";
|
|
22
|
+
readonly MIME_MISMATCH: "A file's bytes, or its data URL's media type, do not match the type the answer declares.";
|
|
23
|
+
readonly MISSING_REQUIRED: "A required field is missing.";
|
|
24
|
+
readonly OPTION_NOT_OFFERED: "The answer selects an option id that the ask did not offer.";
|
|
25
|
+
readonly OTHER_NOT_ALLOWED: "An ask or an answer uses Other where the ask does not allow it.";
|
|
26
|
+
readonly OUT_OF_RANGE: "A form number value or default is below the field's min or above its max.";
|
|
27
|
+
readonly RANGE_INVALID: "A count or length bound is out of range: below its minimum, above what the ask offers, or a minimum above its maximum.";
|
|
28
|
+
readonly REQUIRED_FIELD: "A form answer leaves a required field missing or blank.";
|
|
29
|
+
readonly SELECTION_COUNT: "A default or an answer selects the wrong number of options.";
|
|
30
|
+
readonly TOO_FEW: "A list has fewer items than allowed.";
|
|
31
|
+
readonly TOO_LONG: "A string is longer than allowed.";
|
|
32
|
+
readonly TOO_MANY: "A list has more items than allowed.";
|
|
33
|
+
readonly TOO_SHORT: "A string is empty, only whitespace, or shorter than its minimum.";
|
|
34
|
+
readonly UNKNOWN_BUTTON: "The pressed button is not on the pending ask's simple card.";
|
|
35
|
+
readonly UNKNOWN_KEY: "An object has a field that its schema does not define.";
|
|
36
|
+
};
|
|
37
|
+
export type AskErrorCode = keyof typeof ASK_ERROR_CODES;
|
|
38
|
+
export interface AskValidationError {
|
|
39
|
+
/** A key of `ASK_ERROR_CODES`. */
|
|
40
|
+
code: AskErrorCode;
|
|
41
|
+
/** One instruction that fixes the error. */
|
|
42
|
+
fix: string;
|
|
43
|
+
/** What is wrong, in one sentence. */
|
|
44
|
+
message: string;
|
|
45
|
+
/** Where the error is, such as `options[2].id`. The root is an empty string. */
|
|
46
|
+
path: string;
|
|
47
|
+
}
|
|
48
|
+
/** An error before sorting; `segments` keeps array indexes numeric so they sort correctly. */
|
|
49
|
+
export interface AskErrorDraft {
|
|
50
|
+
code: AskErrorCode;
|
|
51
|
+
fix: string;
|
|
52
|
+
message: string;
|
|
53
|
+
segments: readonly PropertyKey[];
|
|
54
|
+
}
|
|
55
|
+
export declare const formatAskPath: (segments: readonly PropertyKey[]) => string;
|
|
56
|
+
/** A Zod custom issue that carries an ask error code and its fix. */
|
|
57
|
+
export declare const askIssue: ({ code, fix, message, segments, }: AskErrorDraft) => {
|
|
58
|
+
code: "custom";
|
|
59
|
+
message: string;
|
|
60
|
+
params: {
|
|
61
|
+
askCode: AskErrorCode;
|
|
62
|
+
fix: string;
|
|
63
|
+
};
|
|
64
|
+
path: PropertyKey[];
|
|
65
|
+
};
|
|
66
|
+
export declare const quoteValue: (value: unknown) => string;
|
|
67
|
+
/**
|
|
68
|
+
* Maps Zod issues to ask errors. `root` is the value that was parsed; it tells a missing key apart
|
|
69
|
+
* from a key with the wrong type. `prefix` is prepended to every path.
|
|
70
|
+
*/
|
|
71
|
+
export declare const issuesToAskErrors: ({ issues, prefix, root, }: {
|
|
72
|
+
issues: readonly z.core.$ZodIssue[];
|
|
73
|
+
prefix?: readonly PropertyKey[];
|
|
74
|
+
root: unknown;
|
|
75
|
+
}) => AskErrorDraft[];
|
|
76
|
+
/** Sorts drafts by path (array indexes in numeric order), then by code, and drops duplicates. */
|
|
77
|
+
export declare const finalizeAskErrors: (drafts: readonly AskErrorDraft[]) => AskValidationError[];
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.finalizeAskErrors = exports.issuesToAskErrors = exports.quoteValue = exports.askIssue = exports.formatAskPath = exports.ASK_ERROR_CODES = void 0;
|
|
4
|
+
const limits_1 = require("./limits");
|
|
5
|
+
/**
|
|
6
|
+
* Every ask error code, with its meaning. `validateAskInput` and `validateAskResponse` return all
|
|
7
|
+
* of them except `UNKNOWN_BUTTON`, which the headless `turn` endpoint returns, and
|
|
8
|
+
* `FILE_NOT_OWNED`, which the server returns when it loads an uploaded file.
|
|
9
|
+
*/
|
|
10
|
+
exports.ASK_ERROR_CODES = {
|
|
11
|
+
CHANGED_MISMATCH: "A markdown answer's changed flag does not match whether its text differs from the draft.",
|
|
12
|
+
DECLINE_NOT_ALLOWED: "The answer skips an ask that does not allow skipping.",
|
|
13
|
+
DEFAULT_NOT_IN_OPTIONS: "A default names an option id that the ask does not offer.",
|
|
14
|
+
DUPLICATE_ID: "An id appears twice where ids must be unique: options, default, accept, or an answer.",
|
|
15
|
+
DUPLICATE_LABEL: "Two buttons would share a label: options of a compact ask, or a confirm's approve and deny.",
|
|
16
|
+
FIELD_TYPE_MISMATCH: "A form value or default does not fit its field's type: the wrong JSON type, not a whole number, or not a valid email, URL, or phone number.",
|
|
17
|
+
FILE_COUNT: "A files answer has fewer files than minFiles or more than maxFiles.",
|
|
18
|
+
FILE_NOT_OWNED: "A files answer names a fileId that is not an upload of the caller, or the host has no file storage.",
|
|
19
|
+
FILE_TOO_LARGE: "A file is larger than the host's per-file upload cap.",
|
|
20
|
+
FILE_TYPE_NOT_ACCEPTED: "A file's declared type is not one the ask's accept list allows.",
|
|
21
|
+
INVALID_DATE: "A form date, time, or datetime value or default is not a real ISO 8601 value in the field's format.",
|
|
22
|
+
INVALID_ENUM: "A value is not one of the allowed values.",
|
|
23
|
+
INVALID_FORMAT: "A string does not match its required format.",
|
|
24
|
+
INVALID_TYPE: "A value has the wrong type.",
|
|
25
|
+
MIME_MISMATCH: "A file's bytes, or its data URL's media type, do not match the type the answer declares.",
|
|
26
|
+
MISSING_REQUIRED: "A required field is missing.",
|
|
27
|
+
OPTION_NOT_OFFERED: "The answer selects an option id that the ask did not offer.",
|
|
28
|
+
OTHER_NOT_ALLOWED: "An ask or an answer uses Other where the ask does not allow it.",
|
|
29
|
+
OUT_OF_RANGE: "A form number value or default is below the field's min or above its max.",
|
|
30
|
+
RANGE_INVALID: "A count or length bound is out of range: below its minimum, above what the ask offers, or a minimum above its maximum.",
|
|
31
|
+
REQUIRED_FIELD: "A form answer leaves a required field missing or blank.",
|
|
32
|
+
SELECTION_COUNT: "A default or an answer selects the wrong number of options.",
|
|
33
|
+
TOO_FEW: "A list has fewer items than allowed.",
|
|
34
|
+
TOO_LONG: "A string is longer than allowed.",
|
|
35
|
+
TOO_MANY: "A list has more items than allowed.",
|
|
36
|
+
TOO_SHORT: "A string is empty, only whitespace, or shorter than its minimum.",
|
|
37
|
+
UNKNOWN_BUTTON: "The pressed button is not on the pending ask's simple card.",
|
|
38
|
+
UNKNOWN_KEY: "An object has a field that its schema does not define.",
|
|
39
|
+
};
|
|
40
|
+
const MAX_QUOTED_LENGTH = 40;
|
|
41
|
+
const isAskErrorCode = (value) => typeof value === "string" && Object.hasOwn(exports.ASK_ERROR_CODES, value);
|
|
42
|
+
const formatAskPath = (segments) => segments.reduce((path, segment) => {
|
|
43
|
+
if (typeof segment === "number") {
|
|
44
|
+
return `${path}[${segment}]`;
|
|
45
|
+
}
|
|
46
|
+
const key = String(segment);
|
|
47
|
+
return path === "" ? key : `${path}.${key}`;
|
|
48
|
+
}, "");
|
|
49
|
+
exports.formatAskPath = formatAskPath;
|
|
50
|
+
/** A Zod custom issue that carries an ask error code and its fix. */
|
|
51
|
+
const askIssue = ({ code, fix, message, segments, }) => ({ code: "custom", message, params: { askCode: code, fix }, path: [...segments] });
|
|
52
|
+
exports.askIssue = askIssue;
|
|
53
|
+
const quoteValue = (value) => {
|
|
54
|
+
if (typeof value !== "string") {
|
|
55
|
+
return String(value);
|
|
56
|
+
}
|
|
57
|
+
const shortened = value.length > MAX_QUOTED_LENGTH ? `${value.slice(0, MAX_QUOTED_LENGTH - 1)}…` : value;
|
|
58
|
+
return JSON.stringify(shortened);
|
|
59
|
+
};
|
|
60
|
+
exports.quoteValue = quoteValue;
|
|
61
|
+
const sentenceSubject = (path) => (path === "" ? "The value" : path);
|
|
62
|
+
const inlineSubject = (path) => (path === "" ? "the value" : path);
|
|
63
|
+
const describeExpected = (expected) => {
|
|
64
|
+
switch (expected) {
|
|
65
|
+
case "array":
|
|
66
|
+
case "tuple":
|
|
67
|
+
return "a list";
|
|
68
|
+
case "boolean":
|
|
69
|
+
return "true or false";
|
|
70
|
+
case "int":
|
|
71
|
+
return "a whole number";
|
|
72
|
+
case "number":
|
|
73
|
+
return "a number";
|
|
74
|
+
case "map":
|
|
75
|
+
case "object":
|
|
76
|
+
case "record":
|
|
77
|
+
return "an object";
|
|
78
|
+
case "string":
|
|
79
|
+
return "a string";
|
|
80
|
+
default:
|
|
81
|
+
return expected;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
const describeReceived = (value) => {
|
|
85
|
+
if (value === null) {
|
|
86
|
+
return "null";
|
|
87
|
+
}
|
|
88
|
+
if (Array.isArray(value)) {
|
|
89
|
+
return "a list";
|
|
90
|
+
}
|
|
91
|
+
switch (typeof value) {
|
|
92
|
+
case "boolean":
|
|
93
|
+
return "a boolean";
|
|
94
|
+
case "number":
|
|
95
|
+
return "a number";
|
|
96
|
+
case "object":
|
|
97
|
+
return "an object";
|
|
98
|
+
case "string":
|
|
99
|
+
return "a string";
|
|
100
|
+
default:
|
|
101
|
+
return typeof value;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
const isPrimitive = (value) => value === null || ["boolean", "number", "string"].includes(typeof value);
|
|
105
|
+
const lookup = (root, segments) => {
|
|
106
|
+
let current = root;
|
|
107
|
+
for (const segment of segments) {
|
|
108
|
+
if (current === null || typeof current !== "object" || !(segment in current)) {
|
|
109
|
+
return { found: false, value: undefined };
|
|
110
|
+
}
|
|
111
|
+
current = current[segment];
|
|
112
|
+
}
|
|
113
|
+
return { found: current !== undefined, value: current };
|
|
114
|
+
};
|
|
115
|
+
const missingRequired = (segments) => {
|
|
116
|
+
const path = (0, exports.formatAskPath)(segments);
|
|
117
|
+
const lastSegment = segments.at(-1);
|
|
118
|
+
if (typeof lastSegment !== "string") {
|
|
119
|
+
return {
|
|
120
|
+
code: "MISSING_REQUIRED",
|
|
121
|
+
fix: `Send ${inlineSubject(path)}.`,
|
|
122
|
+
message: `${sentenceSubject(path)} is required.`,
|
|
123
|
+
segments,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
const parentPath = (0, exports.formatAskPath)(segments.slice(0, -1));
|
|
127
|
+
return {
|
|
128
|
+
code: "MISSING_REQUIRED",
|
|
129
|
+
fix: parentPath === "" ? `Add "${lastSegment}".` : `Add "${lastSegment}" to ${parentPath}.`,
|
|
130
|
+
message: `${path} is required.`,
|
|
131
|
+
segments,
|
|
132
|
+
};
|
|
133
|
+
};
|
|
134
|
+
const invalidEnum = (segments, allowed, received) => {
|
|
135
|
+
const path = (0, exports.formatAskPath)(segments);
|
|
136
|
+
const allowedText = allowed.length === 1
|
|
137
|
+
? (0, exports.quoteValue)(allowed[0])
|
|
138
|
+
: `one of ${allowed.map((value) => (0, exports.quoteValue)(value)).join(", ")}`;
|
|
139
|
+
const receivedText = isPrimitive(received) ? `, not ${(0, exports.quoteValue)(received)}` : "";
|
|
140
|
+
return {
|
|
141
|
+
code: "INVALID_ENUM",
|
|
142
|
+
fix: `Use ${allowedText}.`,
|
|
143
|
+
message: `${sentenceSubject(path)} must be ${allowedText}${receivedText}.`,
|
|
144
|
+
segments,
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
const tooShort = (segments) => {
|
|
148
|
+
const path = (0, exports.formatAskPath)(segments);
|
|
149
|
+
return {
|
|
150
|
+
code: "TOO_SHORT",
|
|
151
|
+
fix: `Write at least one visible character in ${inlineSubject(path)}.`,
|
|
152
|
+
message: `${sentenceSubject(path)} must contain visible text.`,
|
|
153
|
+
segments,
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
const numberOutOfRange = (segments, bound) => {
|
|
157
|
+
const path = (0, exports.formatAskPath)(segments);
|
|
158
|
+
return {
|
|
159
|
+
code: "RANGE_INVALID",
|
|
160
|
+
fix: `Make ${inlineSubject(path)} ${bound}.`,
|
|
161
|
+
message: `${sentenceSubject(path)} must be ${bound}.`,
|
|
162
|
+
segments,
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
const unionOptions = (issue) => "options" in issue && Array.isArray(issue.options) ? issue.options : [];
|
|
166
|
+
const describeIssue = (issue, root, prefix) => {
|
|
167
|
+
const segments = [...prefix, ...issue.path];
|
|
168
|
+
const path = (0, exports.formatAskPath)(segments);
|
|
169
|
+
const target = lookup(root, issue.path);
|
|
170
|
+
switch (issue.code) {
|
|
171
|
+
case "unrecognized_keys":
|
|
172
|
+
return issue.keys.map((key) => ({
|
|
173
|
+
code: "UNKNOWN_KEY",
|
|
174
|
+
fix: `Remove "${key}".`,
|
|
175
|
+
message: `"${key}" is not a field of ${path === "" ? "this object" : path}.`,
|
|
176
|
+
segments: [...segments, key],
|
|
177
|
+
}));
|
|
178
|
+
case "invalid_type": {
|
|
179
|
+
if (!target.found) {
|
|
180
|
+
return [missingRequired(segments)];
|
|
181
|
+
}
|
|
182
|
+
const expected = describeExpected(issue.expected);
|
|
183
|
+
const received = issue.expected === "int" && typeof target.value === "number"
|
|
184
|
+
? String(target.value)
|
|
185
|
+
: describeReceived(target.value);
|
|
186
|
+
return [
|
|
187
|
+
{
|
|
188
|
+
code: "INVALID_TYPE",
|
|
189
|
+
fix: `Make ${inlineSubject(path)} ${expected}.`,
|
|
190
|
+
message: `${sentenceSubject(path)} must be ${expected}, not ${received}.`,
|
|
191
|
+
segments,
|
|
192
|
+
},
|
|
193
|
+
];
|
|
194
|
+
}
|
|
195
|
+
case "invalid_value":
|
|
196
|
+
if (!target.found) {
|
|
197
|
+
return [missingRequired(segments)];
|
|
198
|
+
}
|
|
199
|
+
return [invalidEnum(segments, issue.values, target.value)];
|
|
200
|
+
case "invalid_union": {
|
|
201
|
+
if (!target.found) {
|
|
202
|
+
return [missingRequired(segments)];
|
|
203
|
+
}
|
|
204
|
+
const allowed = unionOptions(issue);
|
|
205
|
+
if (allowed.length === 0) {
|
|
206
|
+
return [
|
|
207
|
+
{
|
|
208
|
+
code: "INVALID_TYPE",
|
|
209
|
+
fix: `Check ${inlineSubject(path)}.`,
|
|
210
|
+
message: issue.message,
|
|
211
|
+
segments,
|
|
212
|
+
},
|
|
213
|
+
];
|
|
214
|
+
}
|
|
215
|
+
return [invalidEnum(segments, allowed, target.value)];
|
|
216
|
+
}
|
|
217
|
+
case "too_big": {
|
|
218
|
+
const maximum = String(issue.maximum);
|
|
219
|
+
if (issue.origin === "number") {
|
|
220
|
+
return [numberOutOfRange(segments, `at most ${maximum}`)];
|
|
221
|
+
}
|
|
222
|
+
if (issue.origin === "array" || issue.origin === "set") {
|
|
223
|
+
return [
|
|
224
|
+
{
|
|
225
|
+
code: "TOO_MANY",
|
|
226
|
+
fix: `Remove items from ${inlineSubject(path)} until it has ${maximum} or fewer.`,
|
|
227
|
+
message: `${sentenceSubject(path)} has more than ${maximum} items.`,
|
|
228
|
+
segments,
|
|
229
|
+
},
|
|
230
|
+
];
|
|
231
|
+
}
|
|
232
|
+
return [
|
|
233
|
+
{
|
|
234
|
+
code: "TOO_LONG",
|
|
235
|
+
fix: `Shorten ${inlineSubject(path)} to ${maximum} characters or fewer.`,
|
|
236
|
+
message: `${sentenceSubject(path)} is longer than ${maximum} characters.`,
|
|
237
|
+
segments,
|
|
238
|
+
},
|
|
239
|
+
];
|
|
240
|
+
}
|
|
241
|
+
case "too_small": {
|
|
242
|
+
if (issue.origin === "number") {
|
|
243
|
+
return [numberOutOfRange(segments, `at least ${String(issue.minimum)}`)];
|
|
244
|
+
}
|
|
245
|
+
if (issue.origin === "array" || issue.origin === "set") {
|
|
246
|
+
const minimum = String(issue.minimum);
|
|
247
|
+
return [
|
|
248
|
+
{
|
|
249
|
+
code: "TOO_FEW",
|
|
250
|
+
fix: `Add items to ${inlineSubject(path)} until it has at least ${minimum}.`,
|
|
251
|
+
message: `${sentenceSubject(path)} has fewer than ${minimum} items.`,
|
|
252
|
+
segments,
|
|
253
|
+
},
|
|
254
|
+
];
|
|
255
|
+
}
|
|
256
|
+
return [tooShort(segments)];
|
|
257
|
+
}
|
|
258
|
+
case "invalid_format":
|
|
259
|
+
if (issue.pattern === String(limits_1.ASK_LIMITS.choice.optionIdPattern)) {
|
|
260
|
+
const idKind = issue.path.length === 3 && issue.path[0] === "fields" ? "field" : "option";
|
|
261
|
+
return [
|
|
262
|
+
{
|
|
263
|
+
code: "INVALID_FORMAT",
|
|
264
|
+
fix: `Use 1-${limits_1.ASK_LIMITS.choice.optionIdMaxLength} lowercase letters, digits, "_", or "-", starting with a letter or digit.`,
|
|
265
|
+
message: `${sentenceSubject(path)} ${(0, exports.quoteValue)(target.value)} is not a valid ${idKind} id.`,
|
|
266
|
+
segments,
|
|
267
|
+
},
|
|
268
|
+
];
|
|
269
|
+
}
|
|
270
|
+
return [
|
|
271
|
+
{
|
|
272
|
+
code: "INVALID_FORMAT",
|
|
273
|
+
fix: `Change ${inlineSubject(path)} to match the ${issue.format} format.`,
|
|
274
|
+
message: `${sentenceSubject(path)} does not match the ${issue.format} format.`,
|
|
275
|
+
segments,
|
|
276
|
+
},
|
|
277
|
+
];
|
|
278
|
+
case "custom": {
|
|
279
|
+
const askCode = issue.params?.askCode;
|
|
280
|
+
const fix = issue.params?.fix;
|
|
281
|
+
if (askCode === "TOO_SHORT") {
|
|
282
|
+
return [tooShort(segments)];
|
|
283
|
+
}
|
|
284
|
+
if (isAskErrorCode(askCode) && typeof fix === "string") {
|
|
285
|
+
return [{ code: askCode, fix, message: issue.message, segments }];
|
|
286
|
+
}
|
|
287
|
+
return [
|
|
288
|
+
{
|
|
289
|
+
code: "INVALID_FORMAT",
|
|
290
|
+
fix: `Check ${inlineSubject(path)}.`,
|
|
291
|
+
message: issue.message,
|
|
292
|
+
segments,
|
|
293
|
+
},
|
|
294
|
+
];
|
|
295
|
+
}
|
|
296
|
+
default:
|
|
297
|
+
return [
|
|
298
|
+
{
|
|
299
|
+
code: "INVALID_TYPE",
|
|
300
|
+
fix: `Check ${inlineSubject(path)}.`,
|
|
301
|
+
message: issue.message,
|
|
302
|
+
segments,
|
|
303
|
+
},
|
|
304
|
+
];
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
/**
|
|
308
|
+
* Maps Zod issues to ask errors. `root` is the value that was parsed; it tells a missing key apart
|
|
309
|
+
* from a key with the wrong type. `prefix` is prepended to every path.
|
|
310
|
+
*/
|
|
311
|
+
const issuesToAskErrors = ({ issues, prefix = [], root, }) => {
|
|
312
|
+
const typeIssuePaths = new Set(issues
|
|
313
|
+
.filter((issue) => issue.code === "invalid_type")
|
|
314
|
+
.map((issue) => (0, exports.formatAskPath)(issue.path)));
|
|
315
|
+
return issues.flatMap((issue) => {
|
|
316
|
+
if (issue.code !== "invalid_type" && typeIssuePaths.has((0, exports.formatAskPath)(issue.path))) {
|
|
317
|
+
return [];
|
|
318
|
+
}
|
|
319
|
+
return describeIssue(issue, root, prefix);
|
|
320
|
+
});
|
|
321
|
+
};
|
|
322
|
+
exports.issuesToAskErrors = issuesToAskErrors;
|
|
323
|
+
const compareStrings = (left, right) => {
|
|
324
|
+
if (left === right) {
|
|
325
|
+
return 0;
|
|
326
|
+
}
|
|
327
|
+
return left < right ? -1 : 1;
|
|
328
|
+
};
|
|
329
|
+
const compareSegments = (left, right) => {
|
|
330
|
+
const length = Math.min(left.length, right.length);
|
|
331
|
+
for (let index = 0; index < length; index += 1) {
|
|
332
|
+
const a = left[index];
|
|
333
|
+
const b = right[index];
|
|
334
|
+
if (a === b) {
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
if (typeof a === "number" && typeof b === "number") {
|
|
338
|
+
return a - b;
|
|
339
|
+
}
|
|
340
|
+
if (typeof a === "number") {
|
|
341
|
+
return -1;
|
|
342
|
+
}
|
|
343
|
+
if (typeof b === "number") {
|
|
344
|
+
return 1;
|
|
345
|
+
}
|
|
346
|
+
return compareStrings(String(a), String(b));
|
|
347
|
+
}
|
|
348
|
+
return left.length - right.length;
|
|
349
|
+
};
|
|
350
|
+
/** Sorts drafts by path (array indexes in numeric order), then by code, and drops duplicates. */
|
|
351
|
+
const finalizeAskErrors = (drafts) => {
|
|
352
|
+
const sorted = [...drafts].sort((left, right) => compareSegments(left.segments, right.segments) || compareStrings(left.code, right.code));
|
|
353
|
+
const seen = new Set();
|
|
354
|
+
return sorted.flatMap((draft) => {
|
|
355
|
+
const path = (0, exports.formatAskPath)(draft.segments);
|
|
356
|
+
const key = `${path}\u0000${draft.code}`;
|
|
357
|
+
if (seen.has(key)) {
|
|
358
|
+
return [];
|
|
359
|
+
}
|
|
360
|
+
seen.add(key);
|
|
361
|
+
return [{ code: draft.code, fix: draft.fix, message: draft.message, path }];
|
|
362
|
+
});
|
|
363
|
+
};
|
|
364
|
+
exports.finalizeAskErrors = finalizeAskErrors;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type AskErrorDraft, type AskValidationError } from "./errors";
|
|
2
|
+
/** What a `files` ask may accept, and the MIME types each value allows. */
|
|
3
|
+
export declare const ASK_FILE_ACCEPT_MIME_TYPES: {
|
|
4
|
+
readonly csv: readonly ["text/csv"];
|
|
5
|
+
readonly image: readonly ["image/jpeg", "image/png", "image/gif", "image/webp"];
|
|
6
|
+
readonly json: readonly ["application/json"];
|
|
7
|
+
readonly pdf: readonly ["application/pdf"];
|
|
8
|
+
readonly text: readonly ["text/plain"];
|
|
9
|
+
};
|
|
10
|
+
/** The values of a `files` ask's `accept` list, in the order the docs and prompt list them. */
|
|
11
|
+
export declare const ASK_FILE_ACCEPT: readonly ["image", "pdf", "text", "csv", "json"];
|
|
12
|
+
export type AskFileAccept = (typeof ASK_FILE_ACCEPT)[number];
|
|
13
|
+
export type AskFileMimeType = (typeof ASK_FILE_ACCEPT_MIME_TYPES)[AskFileAccept][number];
|
|
14
|
+
/** The MIME types an `accept` list allows, each listed once, in `accept` order. */
|
|
15
|
+
export declare const acceptedFileMimeTypes: (accept: readonly AskFileAccept[]) => AskFileMimeType[];
|
|
16
|
+
/** Whether a file of this type reaches the model as text rather than as bytes. */
|
|
17
|
+
export declare const isTextFileMimeType: (mimeType: string) => boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Splits a base64 `data:` URL into its media type (lowercase, without parameters) and payload.
|
|
20
|
+
* Returns undefined for anything else, including URL-encoded data URLs and remote URLs.
|
|
21
|
+
*/
|
|
22
|
+
export declare const parseAskDataUrl: (url: string) => {
|
|
23
|
+
base64: string;
|
|
24
|
+
mediaType: string;
|
|
25
|
+
} | undefined;
|
|
26
|
+
/** The number of bytes a base64 payload decodes to. */
|
|
27
|
+
export declare const base64ByteLength: (base64: string) => number;
|
|
28
|
+
/** What the bytes of a file are: one of the accepted binary types, UTF-8 text, or unknown. */
|
|
29
|
+
export type SniffedFileType = "application/pdf" | "image/gif" | "image/jpeg" | "image/png" | "image/webp" | "text" | "unknown";
|
|
30
|
+
/**
|
|
31
|
+
* Reads a file's type from its first bytes. Images and PDFs are matched by their signatures;
|
|
32
|
+
* anything else that is valid UTF-8 with no NUL character is text.
|
|
33
|
+
*/
|
|
34
|
+
export declare const sniffFileBytes: (bytes: Uint8Array) => SniffedFileType;
|
|
35
|
+
export declare const fileTooLargeDraft: ({ maxFileSizeBytes, segments, size, }: {
|
|
36
|
+
maxFileSizeBytes: number;
|
|
37
|
+
segments: readonly PropertyKey[];
|
|
38
|
+
size: number;
|
|
39
|
+
}) => AskErrorDraft;
|
|
40
|
+
/**
|
|
41
|
+
* Checks the bytes of the file at `content.files[index]` on the server: they must fit the per-file
|
|
42
|
+
* cap and match the declared MIME type (a JSON file must also parse).
|
|
43
|
+
*/
|
|
44
|
+
export declare const checkAskFileBytes: ({ bytes, index, maxFileSizeBytes, mimeType, }: {
|
|
45
|
+
bytes: Uint8Array;
|
|
46
|
+
index: number;
|
|
47
|
+
maxFileSizeBytes: number;
|
|
48
|
+
mimeType: string;
|
|
49
|
+
}) => AskValidationError[];
|
|
50
|
+
/** The error for a `fileId` that the caller did not upload, or that the host cannot load. */
|
|
51
|
+
export declare const fileNotOwnedError: ({ index }: {
|
|
52
|
+
index: number;
|
|
53
|
+
}) => AskValidationError;
|