@case-framework/survey-assistant 0.1.0
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/README.md +212 -0
- package/dist/authoring-references-Cy8J-DWt.d.mts +48 -0
- package/dist/authoring-references-Cy8J-DWt.d.mts.map +1 -0
- package/dist/authoring-references-X6uRaWuf.mjs +68 -0
- package/dist/authoring-references-X6uRaWuf.mjs.map +1 -0
- package/dist/capabilities-B6tvE7fK.mjs +66 -0
- package/dist/capabilities-B6tvE7fK.mjs.map +1 -0
- package/dist/capabilities-DvSKtjAw.d.mts +182 -0
- package/dist/capabilities-DvSKtjAw.d.mts.map +1 -0
- package/dist/capabilities-default.d.mts +15 -0
- package/dist/capabilities-default.d.mts.map +1 -0
- package/dist/capabilities-default.mjs +347 -0
- package/dist/capabilities-default.mjs.map +1 -0
- package/dist/constants-CFUWrRtl.mjs +7 -0
- package/dist/constants-CFUWrRtl.mjs.map +1 -0
- package/dist/constants-Jw0k8ojS.d.mts +67 -0
- package/dist/constants-Jw0k8ojS.d.mts.map +1 -0
- package/dist/controller-proxy-BRDusFiO.d.mts +57 -0
- package/dist/controller-proxy-BRDusFiO.d.mts.map +1 -0
- package/dist/controller-proxy-Duxo2IFE.mjs +92 -0
- package/dist/controller-proxy-Duxo2IFE.mjs.map +1 -0
- package/dist/digest-BsoVq49o.d.mts +9 -0
- package/dist/digest-BsoVq49o.d.mts.map +1 -0
- package/dist/digest.d.mts +2 -0
- package/dist/digest.mjs +20 -0
- package/dist/digest.mjs.map +1 -0
- package/dist/engine-sPvuixWg.mjs +4857 -0
- package/dist/engine-sPvuixWg.mjs.map +1 -0
- package/dist/engine.d.mts +3 -0
- package/dist/engine.mjs +4 -0
- package/dist/index-BWNHkpkh.d.mts +187 -0
- package/dist/index-BWNHkpkh.d.mts.map +1 -0
- package/dist/index-CUqbBjkc.d.mts +641 -0
- package/dist/index-CUqbBjkc.d.mts.map +1 -0
- package/dist/index-MpEMQzTm.d.mts +123 -0
- package/dist/index-MpEMQzTm.d.mts.map +1 -0
- package/dist/index-TFtvBiSQ.d.mts +1149 -0
- package/dist/index-TFtvBiSQ.d.mts.map +1 -0
- package/dist/memory-thread-repository-CJkkVE2-.mjs +718 -0
- package/dist/memory-thread-repository-CJkkVE2-.mjs.map +1 -0
- package/dist/protocol-DV0ka9WT.mjs +1032 -0
- package/dist/protocol-DV0ka9WT.mjs.map +1 -0
- package/dist/protocol.d.mts +3 -0
- package/dist/protocol.mjs +2 -0
- package/dist/react-VuYe-cPh.mjs +521 -0
- package/dist/react-VuYe-cPh.mjs.map +1 -0
- package/dist/react-integration.d.mts +2 -0
- package/dist/react-integration.mjs +3 -0
- package/dist/react.d.mts +3 -0
- package/dist/react.mjs +4 -0
- package/dist/references/assistant-operations.md +287 -0
- package/dist/references/core-rules.md +11 -0
- package/dist/references/embedded-forms.md +80 -0
- package/dist/references/expressions.md +301 -0
- package/dist/references/item-types.md +364 -0
- package/dist/references/localization.md +25 -0
- package/dist/references/rich-text-content.md +193 -0
- package/dist/references/source-material-surveys.md +60 -0
- package/dist/references/survey-data-model.md +109 -0
- package/dist/references/survey-quality.md +23 -0
- package/dist/request-context-jcmXT_Q2.mjs +44 -0
- package/dist/request-context-jcmXT_Q2.mjs.map +1 -0
- package/dist/request-guardrails-C59IWnH8.mjs +73 -0
- package/dist/request-guardrails-C59IWnH8.mjs.map +1 -0
- package/dist/server-agent.d.mts +131 -0
- package/dist/server-agent.d.mts.map +1 -0
- package/dist/server-agent.mjs +229 -0
- package/dist/server-agent.mjs.map +1 -0
- package/dist/server-runtime.d.mts +123 -0
- package/dist/server-runtime.d.mts.map +1 -0
- package/dist/server-runtime.mjs +296 -0
- package/dist/server-runtime.mjs.map +1 -0
- package/dist/server-tasks.d.mts +78 -0
- package/dist/server-tasks.d.mts.map +1 -0
- package/dist/server-tasks.mjs +253 -0
- package/dist/server-tasks.mjs.map +1 -0
- package/dist/server-tools.d.mts +2 -0
- package/dist/server-tools.mjs +2 -0
- package/dist/server.d.mts +52 -0
- package/dist/server.d.mts.map +1 -0
- package/dist/server.mjs +6 -0
- package/dist/storage-postgres.d.mts +37 -0
- package/dist/storage-postgres.d.mts.map +1 -0
- package/dist/storage-postgres.mjs +167 -0
- package/dist/storage-postgres.mjs.map +1 -0
- package/dist/thread-documents-DamIq9Od.d.mts +91 -0
- package/dist/thread-documents-DamIq9Od.d.mts.map +1 -0
- package/dist/thread-handlers-DpynUc64.d.mts +112 -0
- package/dist/thread-handlers-DpynUc64.d.mts.map +1 -0
- package/dist/tools-Hz2p7vyv.mjs +1879 -0
- package/dist/tools-Hz2p7vyv.mjs.map +1 -0
- package/dist/ui.css +2 -0
- package/dist/ui.d.mts +63 -0
- package/dist/ui.d.mts.map +1 -0
- package/dist/ui.mjs +2238 -0
- package/dist/ui.mjs.map +1 -0
- package/package.json +166 -0
- package/ui.css.d.ts +3 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Survey Localization
|
|
2
|
+
|
|
3
|
+
Use this reference when the user asks to translate, localize, add a language, or make all/some survey content available in another language.
|
|
4
|
+
|
|
5
|
+
## Locale Lifecycle
|
|
6
|
+
|
|
7
|
+
- A target locale may be absent. That is a normal starting state, not an error and not a reason to inspect a nonexistent translation branch.
|
|
8
|
+
- Use the target locale code directly in typed translation operations. The first applied translation creates the locale branch while preserving every existing locale.
|
|
9
|
+
- Never create a temporary item, placeholder text, blank title, or dummy translation merely to introduce a locale. Introduce it only through the real respondent-visible translations requested by the user.
|
|
10
|
+
- Never put target-language respondent text into an existing source-language locale just because that locale already exists.
|
|
11
|
+
- Coding keys and editor-only item labels are not respondent translations. Translate them only when the user explicitly asks.
|
|
12
|
+
|
|
13
|
+
## Scope A Complete Localization Correctly
|
|
14
|
+
|
|
15
|
+
1. Inspect the complete outline. Page it when needed; do not assume a compact outline is the entire survey.
|
|
16
|
+
2. Inspect each relevant item type/configuration to discover every respondent-visible content key. This includes question titles/subtitles/info copy, choice-option labels, form-field labels, select-option labels, embedded-form labels, validation copy, and respondent-facing navigation/card copy when present.
|
|
17
|
+
3. Preserve the source language while writing each target-language value under the target locale. Do not replace source content or translation containers.
|
|
18
|
+
4. Use `update-item-translation` for ordinary item content. Use typed choice operations only when also changing the choice structure. Use a focused raw patch only for a supported survey-level translation surface that has no typed operation.
|
|
19
|
+
5. Before proposing, check that every visible label implied by the inspected item configuration has a target-locale translation. A title alone is not a complete localization of a choice or form item.
|
|
20
|
+
|
|
21
|
+
## Bounded Execution
|
|
22
|
+
|
|
23
|
+
- Prefer one atomic proposal for a coherent localization when it fits the advertised operation limit. The limit is a hard safety bound, not a target chunk size.
|
|
24
|
+
- Split only an oversized localization into independent, coherent chunks by section or item set. Independent localization chunks may be prepared in the same turn; do not make a later chunk depend on an unapplied structural change.
|
|
25
|
+
- If live inspection or a source translation cannot determine a phrase accurately, ask a focused clarification rather than inventing domain-specific wording.
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# Rich Text Content
|
|
2
|
+
|
|
3
|
+
Formatted survey text uses `{ "type": "richText", "version": 1, "doc": ... }`.
|
|
4
|
+
Do not use markdown syntax for formatting; `md` content is displayed as plain text.
|
|
5
|
+
|
|
6
|
+
Use typed `update-item-translation` and `create-item.translations[].content` for formatted text. Prefer `plainText` for all unformatted text, including a simple title, label, or one-paragraph info item. Use richText only when formatting carries meaning or structure. Use `survey-json-patch` for rich text only when you need to edit raw paths that typed translation operations cannot address.
|
|
7
|
+
|
|
8
|
+
For `survey_change`, compact `richTextBlocks` shorthand is accepted and normalized before validation. This is useful for source-document imports:
|
|
9
|
+
|
|
10
|
+
```json
|
|
11
|
+
{
|
|
12
|
+
"locale": "en",
|
|
13
|
+
"contentKey": "content",
|
|
14
|
+
"richTextBlocks": [
|
|
15
|
+
{ "type": "heading", "level": 2, "text": "How should I complete this survey?" },
|
|
16
|
+
{ "type": "paragraph", "text": "Read each question carefully before answering." },
|
|
17
|
+
{ "type": "bulletList", "items": ["Select the answer that applies to you.", "Enter the requested information."] },
|
|
18
|
+
{ "type": "footnote", "text": "* Complete the survey using the information for the intended participant." }
|
|
19
|
+
]
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The shorthand also supports inline runs through `children`. Use it when a paragraph or question title contains a link, bold/italic/underlined words, or a mix of styles:
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"locale": "en",
|
|
28
|
+
"contentKey": "title",
|
|
29
|
+
"richTextBlocks": [
|
|
30
|
+
{
|
|
31
|
+
"type": "paragraph",
|
|
32
|
+
"children": [
|
|
33
|
+
{ "type": "text", "text": "Read the ", "style": { "weight": "strong" } },
|
|
34
|
+
{ "type": "text", "text": "guidance" },
|
|
35
|
+
{ "type": "text", "text": " at " },
|
|
36
|
+
{ "type": "link", "href": "https://example.org/help", "text": "example.org/help" }
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
For an email address, use `href: "mailto:contact@example.org"`. Do not put a visible URL or email address into a plain-text block when the source intends it to be clickable.
|
|
44
|
+
|
|
45
|
+
## Inline Nodes
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{ "type": "text", "text": "Important", "style": { "weight": "strong" } }
|
|
49
|
+
{ "type": "text", "text": "accented", "style": { "color": "accent" } }
|
|
50
|
+
{ "type": "template", "key": "participantName" }
|
|
51
|
+
{ "type": "lineBreak" }
|
|
52
|
+
{
|
|
53
|
+
"type": "link",
|
|
54
|
+
"href": "https://example.com",
|
|
55
|
+
"children": [{ "type": "text", "text": "Read more" }]
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Important:
|
|
60
|
+
- `template` inline nodes render template-variable tokens such as `{participantName}`. They are not respondent inputs and must not be used to simulate cloze blanks or inline answer fields.
|
|
61
|
+
- The current editor/player path ignores generic `inlineExtension` nodes. Do not invent custom inline node types unless the registry explicitly supports them.
|
|
62
|
+
|
|
63
|
+
Style values:
|
|
64
|
+
- `weight`: `strong` or `normal`
|
|
65
|
+
- `italic`: `italic` or `normal`
|
|
66
|
+
- `underline`: `underline` or `none`
|
|
67
|
+
- `color`: `primary`, `accent`, or `default`
|
|
68
|
+
|
|
69
|
+
## Block Nodes
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"type": "richText",
|
|
74
|
+
"version": 1,
|
|
75
|
+
"doc": {
|
|
76
|
+
"type": "doc",
|
|
77
|
+
"blocks": [
|
|
78
|
+
{
|
|
79
|
+
"type": "heading",
|
|
80
|
+
"level": 2,
|
|
81
|
+
"children": [{ "type": "text", "text": "Before you start" }]
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"type": "paragraph",
|
|
85
|
+
"children": [
|
|
86
|
+
{ "type": "text", "text": "Read this " },
|
|
87
|
+
{ "type": "text", "text": "carefully", "style": { "weight": "strong" } },
|
|
88
|
+
{ "type": "text", "text": " before continuing." }
|
|
89
|
+
]
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"type": "bulletList",
|
|
93
|
+
"items": [
|
|
94
|
+
{
|
|
95
|
+
"type": "listItem",
|
|
96
|
+
"children": [
|
|
97
|
+
{ "type": "paragraph", "children": [{ "type": "text", "text": "First point" }] }
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
]
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"type": "infoBox",
|
|
104
|
+
"tone": "info",
|
|
105
|
+
"title": [{ "type": "text", "text": "Note" }],
|
|
106
|
+
"collapsible": true,
|
|
107
|
+
"defaultOpen": true,
|
|
108
|
+
"children": [
|
|
109
|
+
{ "type": "paragraph", "children": [{ "type": "text", "text": "Helpful detail." }] }
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
]
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Supported block types:
|
|
118
|
+
- `paragraph`
|
|
119
|
+
- `heading` with `level` 1 for section headings or `level` 2 for subsection headings. Do not author levels 3 through 6; the player offsets/clamps these stored levels to semantic page headings.
|
|
120
|
+
- `bulletList` with `listItem` entries
|
|
121
|
+
- `image` with `source: { "type": "asset", "assetId": "..." }` or `source: { "type": "external", "url": "..." }`
|
|
122
|
+
- `infoBox` with `tone: "info"` or `"warning"`
|
|
123
|
+
- `separator`: use only between two substantive blocks within the same content surface. Never use it as the first or last block, or to decorate/mark the boundary of a survey item, page, or section; the survey layout already provides those boundaries.
|
|
124
|
+
|
|
125
|
+
## Where Rich Text Is Used
|
|
126
|
+
|
|
127
|
+
- Info items: content key `content`; full block editor with paragraphs, headings, bullet lists, images, separators, and info boxes.
|
|
128
|
+
- Choice item `topContent` / `bottomContent`: block rich text with paragraphs, headings, bullet lists, images, separators, and info boxes.
|
|
129
|
+
- Form item `topContent` / `bottomContent`: block rich text with paragraphs, bullet lists, images, and separators only. Do not use headings or info boxes there.
|
|
130
|
+
- Question `title` and `cardFooter`: compact rich text with paragraph blocks only. Inline styles may use bold, italic, underline, and color.
|
|
131
|
+
- Question `subtitle`: compact rich text with paragraph blocks only. Inline styles may use bold, underline, and color. Do not rely on italic in subtitles.
|
|
132
|
+
- If the user asks for unsupported formatting on a surface, explain the limitation briefly and use the nearest supported representation.
|
|
133
|
+
|
|
134
|
+
## Updating Existing Rich Text
|
|
135
|
+
|
|
136
|
+
Inspect the item first, then use `update-item-translation` with a rich text `content` value:
|
|
137
|
+
|
|
138
|
+
```json
|
|
139
|
+
{
|
|
140
|
+
"kind": "update-item-translation",
|
|
141
|
+
"itemId": "info_1",
|
|
142
|
+
"locale": "en",
|
|
143
|
+
"contentKey": "content",
|
|
144
|
+
"content": {
|
|
145
|
+
"type": "richText",
|
|
146
|
+
"version": 1,
|
|
147
|
+
"doc": {
|
|
148
|
+
"type": "doc",
|
|
149
|
+
"blocks": [
|
|
150
|
+
{ "type": "heading", "level": 2, "children": [{ "type": "text", "text": "Instructions" }] },
|
|
151
|
+
{ "type": "paragraph", "children": [{ "type": "text", "text": "Please read carefully.", "style": { "weight": "strong" } }] }
|
|
152
|
+
]
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
This replaces that translation key for the target locale. Preserve existing blocks you still want; do not send only the new block unless the user asked to replace the whole content.
|
|
159
|
+
|
|
160
|
+
## Creating A Formatted Info Item
|
|
161
|
+
|
|
162
|
+
For a new formatted info item, use one typed `create-item` operation with a rich text `content` translation:
|
|
163
|
+
|
|
164
|
+
```json
|
|
165
|
+
{
|
|
166
|
+
"kind": "create-item",
|
|
167
|
+
"target": { "parentItemId": "<root-group-id-or-key>", "index": 0 },
|
|
168
|
+
"item": { "id": "info_intro", "key": "intro", "itemType": "infoItem", "config": {} },
|
|
169
|
+
"translations": [
|
|
170
|
+
{
|
|
171
|
+
"locale": "en",
|
|
172
|
+
"contentKey": "content",
|
|
173
|
+
"content": {
|
|
174
|
+
"type": "richText",
|
|
175
|
+
"version": 1,
|
|
176
|
+
"doc": {
|
|
177
|
+
"type": "doc",
|
|
178
|
+
"blocks": [
|
|
179
|
+
{ "type": "heading", "level": 2, "children": [{ "type": "text", "text": "Before you start" }] },
|
|
180
|
+
{ "type": "paragraph", "children": [{ "type": "text", "text": "This survey asks about your preferences." }] }
|
|
181
|
+
]
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
]
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
The raw item can still be minimal:
|
|
190
|
+
|
|
191
|
+
```json
|
|
192
|
+
{ "id": "info_intro", "key": "intro", "itemType": "infoItem", "config": {} }
|
|
193
|
+
```
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Source-Material Survey Implementation
|
|
2
|
+
|
|
3
|
+
Use this reference when the user asks to implement, recreate, convert, import, or continue a survey from a PDF, document, image/OCR text, pasted questionnaire, table, form, protocol, or supplied question list.
|
|
4
|
+
|
|
5
|
+
## Language
|
|
6
|
+
|
|
7
|
+
- Respond to the user in the language of their current message unless they explicitly request another reply language.
|
|
8
|
+
- Write respondent-visible survey text in the language of the supplied source material or supplied questions. If no source language is clear, use the language requested by the user.
|
|
9
|
+
- If the needed locale is missing, introduce it through the created/updated translations. Do not put Dutch/German/French/etc. survey text under `en` just because `en` exists.
|
|
10
|
+
- Preserve source-language answer labels and field labels unless the user asks for translation or localization.
|
|
11
|
+
|
|
12
|
+
## Fidelity And Completeness
|
|
13
|
+
|
|
14
|
+
- First classify source elements as respondent survey content, respondent input, study/admin metadata, print-only logistics, examples, or decorative layout. Preserve respondent content and inputs; adapt or omit print-only/admin/decorative elements unless the user explicitly asks for a literal archive copy.
|
|
15
|
+
- Implement every distinct question, instruction, answer option, section heading, and respondent input implied by the source unless the user explicitly asks for a summary.
|
|
16
|
+
- Do not collapse multiple real questions into one text field. A text field is appropriate only when the source asks for open-ended text, an unsupported structured response cannot be represented more precisely, or the user explicitly asks for a free-text capture.
|
|
17
|
+
- If the questionnaire body asks the respondent to record information, the digital survey must include an input control for it. Do not leave respondent-entered information as static text.
|
|
18
|
+
- Empty lines, blanks, underscores, table cells intended for handwriting, parenthetical "specify" prompts, and inline write-in areas in actual questions imply an input field. Model them as form fields or embedded option forms as appropriate.
|
|
19
|
+
- Do not create respondent questions for front-cover study identifiers, barcodes, prefilled participant names, prefilled dates of birth, page numbers, institutional logos, or duplicate administrative fields when those values should come from digital participant metadata or are asked later as real survey questions.
|
|
20
|
+
- Printed examples that demonstrate how to fill in a paper form should be preserved as instructional text when useful, not converted into real inputs.
|
|
21
|
+
- Preserve question order, numbering intent, and section structure. For long sources, use multiple validated change proposals rather than omitting details. Omit `continuationMode` only on the first mutation call; use `continuationMode: "append"` on every later independent chunk so all chunks are retained.
|
|
22
|
+
- Do not use representative sampling, "and similar items", broad paraphrase, or shortened tables for source-derived surveys unless the user explicitly asks for an abbreviated version. Repeated rows, repeated follow-ups, and long option sets still need to be modeled.
|
|
23
|
+
- If the source is too long to hold comfortably in context, work by source/page/section chunks. For each chunk, make a complete implementation decision for every respondent-facing element before moving on.
|
|
24
|
+
- Keep each source-import `survey_change` payload bounded. A good chunk is one source page/section or about 4-8 respondent items, including their options, fields, footnotes, and conditions. Continue with additional chunks instead of constructing one very large tool call.
|
|
25
|
+
|
|
26
|
+
## Item Mapping
|
|
27
|
+
|
|
28
|
+
- Map supported item types directly. Use `choiceItem` for single choice, multiple choice, and scale-like questions until a dedicated scale item exists.
|
|
29
|
+
- For scale questions, create an ordered `choiceItem` with stable option ids/keys and labels that preserve the endpoints and intermediate labels. Include the scale instruction in `subtitle` or `topContent` when it is not part of the actual question.
|
|
30
|
+
- Use `formItem` for grouped personal data, tables of fields, repeated short inputs, dates, numbers, contact details, or unsupported compound controls.
|
|
31
|
+
- Use choice option embedded forms for choices that include an open field, such as "Other, specify", "Yes, explain", or an option-specific quantity/date/text input.
|
|
32
|
+
- Preserve option-level parenthetical instructions, side notes, and footnote markers. If the current choice option UI has no separate option-description surface, fold short option notes into the visible option label, for example `Uitgenodigde persoon zelf (zie hiervoor de naam op de brief en/of geboortedatum op de voorkant van de vragenlijst)`, and keep markers such as `Iemand anders*`.
|
|
33
|
+
- For sentence-completion options with an embedded input, put the completion cue on the embedded field label, not twice. Source text like `Iemand anders*, namelijk ____`, `Andere bedrijfstak, namelijk ____`, or `Other, please specify ____` should become option label `Iemand anders*` / `Andere bedrijfstak` / `Other` and embedded field label `Namelijk` / `Please specify`.
|
|
34
|
+
- Use info items for non-interactive section introductions, explanations, consent/instruction text, and visible page/section headings. Keep a simple one-paragraph info item as plainText. Use richText only when the source requires headings, lists, links, separators, or meaningful emphasis.
|
|
35
|
+
- Preserve major chapters/sections as survey structure. If page boundaries matter, use available page-break structure and/or a heading info item so respondents see the section break.
|
|
36
|
+
- Include respondent-facing "before you begin", "how to complete this survey", consent, eligibility, safety, and contact/help sections as info items when still relevant digitally. Rewrite or omit paper-only return/postage instructions unless the user wants a literal reproduction.
|
|
37
|
+
|
|
38
|
+
## Conditions And Follow-Ups
|
|
39
|
+
|
|
40
|
+
- Implement implied conditions, branching, conditional follow-up questions, conditional option visibility, and skip logic when the source text clearly implies them. Do not skip conditions just because they require extra work.
|
|
41
|
+
- If newly created items depend on other newly created items, place displayConditions/disabledConditions/validations/prefills directly on the created item objects in the same survey_change call when possible.
|
|
42
|
+
- Load the expressions reference and use the expression tool for complex expression work or existing/applied items.
|
|
43
|
+
|
|
44
|
+
## Text Shaping
|
|
45
|
+
|
|
46
|
+
- Rephrase only enough to make the digital survey clear and grammatically coherent. Preserve measurement meaning, recall periods, answer scales, and eligibility wording.
|
|
47
|
+
- Split source text into the right visible content keys: concise question in `title`; instructions like "multiple answers possible" or recall-period guidance in `subtitle` or `topContent`; explanations after the control in `bottomContent`; non-question prose in info item `content`.
|
|
48
|
+
- Use plainText for ordinary unformatted prose. Use richText, not plainText, only when info item content has headings, subheadings, boxed sections, bullet/list structure, examples, emphasized phrases, separators, footnotes, URLs, email links, or mixed inline formatting. Preserve the hierarchy with heading blocks, paragraph blocks, bullet lists, info boxes, or separators as supported.
|
|
49
|
+
- Preserve meaningful inline source formatting, not only block layout. A URL or email address that is intended as a destination must be a richText `link` node with a canonical `href`; do not rely on automatic link detection. If source text emphasizes or highlights individual words in a question title, subtitle, option, or info text, author that content key as richText with styled inline runs instead of flattening it to plainText. Use emphasis only where it carries source meaning or scanning value.
|
|
50
|
+
- For source-import info items, you may use the compact `richTextBlocks` shorthand in a translation; `survey_change` normalizes it to strict richText. A paragraph or heading can use `children` for mixed inline text, links, and styles. Example blocks: `{ "type": "heading", "level": 2, "text": "Before you begin" }`, `{ "type": "paragraph", "children": [{ "type": "text", "text": "More information: " }, { "type": "link", "href": "https://example.org/help", "text": "example.org/help" }] }`, `{ "type": "paragraph", "children": [{ "type": "text", "text": "Important", "bold": true }, { "type": "text", "text": " wording" }] }`, `{ "type": "bulletList", "items": ["...", "..."] }`, `{ "type": "footnote", "text": "* ..." }`, `{ "type": "separator" }`.
|
|
51
|
+
- Preserve footnote markers and footnote text when they change interpretation, eligibility, instructions, privacy, or coding. If a question/option has `*`, keep the marker in the visible title/option text and place the note in `subtitle`, `topContent`, `bottomContent`, or a following info item close to the referenced question. Do not silently drop source footnotes.
|
|
52
|
+
- Do not put respondent-visible option labels, form field labels, dropdown option labels, validation messages, or embedded-form labels only in config. They need translation entries.
|
|
53
|
+
|
|
54
|
+
## Workflow
|
|
55
|
+
|
|
56
|
+
- For complex source implementation, load item-types and assistant-operations before preparing create-item operations; load rich-text-content before formatted info items; load expressions before complex branching.
|
|
57
|
+
- Use survey_inspect for the root group and long-survey insertion points. Use full outline paging or children lookup; do not guess append indexes from truncated context.
|
|
58
|
+
- Before calling survey_change for a source-derived chunk, do a quick fidelity pass against the source: every question title, option label, option parenthetical, blank/input, section heading, footnote marker, and footnote text in that chunk should either be represented digitally or intentionally omitted as print-only/admin material.
|
|
59
|
+
- If the fidelity pass finds missing details, fix the operations before calling survey_change. Do not knowingly leave cleanup for the user when the source gives enough information to implement it now.
|
|
60
|
+
- Prefer complete, coherent chunks over one enormous proposal. Each chunk should be valid, ordered, and usable on its own. If a page/section is too large for one bounded chunk, split it at a natural subsection boundary and say what remains to continue.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Survey Data Model
|
|
2
|
+
|
|
3
|
+
## RawSurvey
|
|
4
|
+
|
|
5
|
+
A survey JSON object has this shape:
|
|
6
|
+
|
|
7
|
+
```json
|
|
8
|
+
{
|
|
9
|
+
"$schema": "https://github.com/case-framework/case-survey-toolkit/packages/survey-core/schemas/survey-schema.json",
|
|
10
|
+
"maxItemsPerPage": { "large": 1, "small": 1 },
|
|
11
|
+
"surveyItems": [],
|
|
12
|
+
"assets": {},
|
|
13
|
+
"metadata": {},
|
|
14
|
+
"templateValues": {},
|
|
15
|
+
"translations": {}
|
|
16
|
+
}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Required fields:
|
|
20
|
+
- `$schema`: keep the current schema string; never remove or edit it.
|
|
21
|
+
- `surveyItems`: array of raw survey items.
|
|
22
|
+
|
|
23
|
+
Optional fields:
|
|
24
|
+
- `maxItemsPerPage`: object with numeric `large` and `small` page-size limits.
|
|
25
|
+
- `metadata`: survey-level string map.
|
|
26
|
+
- `templateValues`: reusable expression values; see expressions.md.
|
|
27
|
+
- `assets`: image assets keyed by asset id.
|
|
28
|
+
- `translations`: locale-keyed translated content.
|
|
29
|
+
|
|
30
|
+
## RawSurveyItem
|
|
31
|
+
|
|
32
|
+
Minimal item shape:
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"id": "stable-unique-id",
|
|
37
|
+
"key": "coding_key",
|
|
38
|
+
"itemType": "choiceItem"
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Conventions:
|
|
43
|
+
- `id` is globally unique and is the durable reference used by groups, translations, response slots, expressions, and patches.
|
|
44
|
+
- `key` is the coding key shown in fullKey paths. Sibling keys must be unique and use letters, numbers, underscores, or hyphens.
|
|
45
|
+
- `itemType` must exist in the current editor registry.
|
|
46
|
+
|
|
47
|
+
Optional fields:
|
|
48
|
+
- `metadata`: string map. `metadata.itemLabel` is the internal editor label, not respondent-visible text. `metadata.editorItemColor` stores the editor item color as a hex string such as `#0369a1`.
|
|
49
|
+
- `config`: item-type-specific configuration. Many item types need config; simple structural/content items can be sparse.
|
|
50
|
+
- `validations`: map of validation key to `JsonExpression`.
|
|
51
|
+
- `displayConditions`: item/root and component display expressions.
|
|
52
|
+
- `disabledConditions`: component disabled expressions.
|
|
53
|
+
- `prefills`: configured prefill rules; see expressions.md.
|
|
54
|
+
|
|
55
|
+
## Tree Structure
|
|
56
|
+
|
|
57
|
+
There must be exactly one root group:
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"itemType": "group",
|
|
62
|
+
"config": { "isRoot": true, "items": [], "shuffleItems": false }
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Groups order children by id in `config.items`. To reorder survey items, patch the parent group item at `/surveyItems/<groupIndex>/config/items`.
|
|
67
|
+
|
|
68
|
+
## Translations And Content
|
|
69
|
+
|
|
70
|
+
Visible text is stored under translations, not usually in item config:
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"translations": {
|
|
75
|
+
"en": {
|
|
76
|
+
"itemTranslations": {
|
|
77
|
+
"<itemId>": {
|
|
78
|
+
"title": { "type": "md", "content": "Question text" }
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
"surveyCardContent": {},
|
|
82
|
+
"navigationContent": {},
|
|
83
|
+
"validationMessages": {}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Content values may be:
|
|
90
|
+
- `{ "type": "md", "content": "Text" }`
|
|
91
|
+
- `{ "type": "plain", "content": "Plain text" }`
|
|
92
|
+
- `{ "type": "richText", "version": 1, "doc": { "type": "doc", "blocks": [...] } }`
|
|
93
|
+
|
|
94
|
+
Important:
|
|
95
|
+
- `md` and `plain` content are rendered as plain text with line breaks. Markdown syntax such as `# Heading`, `**bold**`, or `- item` is not parsed into formatting.
|
|
96
|
+
- For unformatted text, use typed translation operations with `plainText`.
|
|
97
|
+
- For formatted content, use typed translation operations with a `content` value whose type is `richText`; see rich-text-content.md.
|
|
98
|
+
- If the user asks for another language or the source material is clearly in another language, write respondent-visible translations under that locale code. A translation operation may introduce a missing locale; do not store Dutch, German, French, etc. text under `en` just because `en` exists.
|
|
99
|
+
|
|
100
|
+
Common item content keys:
|
|
101
|
+
- `title`: question title or group/page title.
|
|
102
|
+
- `subtitle`: question subtitle.
|
|
103
|
+
- `topContent`: content before the response controls.
|
|
104
|
+
- `bottomContent`: content after the response controls.
|
|
105
|
+
- `cardFooter`: footer inside the question card.
|
|
106
|
+
- `content`: info item body.
|
|
107
|
+
|
|
108
|
+
JSON Patch paths are relative to the survey document root. Do not prefix paths with `/survey`.
|
|
109
|
+
Escape JSON Pointer segments: `/` becomes `~1`, `~` becomes `~0`.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Survey quality review
|
|
2
|
+
|
|
3
|
+
Use this reference for qualitative feedback on a questionnaire. Separate deterministic technical defects from judgment about research quality. A clean technical audit does not establish reliability, validity, absence of bias, or fitness for a scientific or regulatory purpose.
|
|
4
|
+
|
|
5
|
+
## Review frame
|
|
6
|
+
|
|
7
|
+
1. **Purpose and coverage.** Identify the survey's intended decisions, population, constructs, and mode. Check that every question contributes to an objective and that important constructs are not omitted.
|
|
8
|
+
2. **Cognitive response process.** Consider how respondents comprehend the question, retrieve relevant information, form a judgment, and map it to the offered response. Flag vague concepts, hidden assumptions, unrealistic recall, and response options that do not fit likely answers.
|
|
9
|
+
3. **Wording.** Prefer plain, specific, neutral language. Flag double-barrelled questions, leading or loaded wording, double negatives, unexplained terms, absolutes, presuppositions, and questions respondents may not be able or willing to answer accurately.
|
|
10
|
+
4. **Response design.** Check that categories are mutually exclusive and sufficiently exhaustive, scales are balanced and consistently oriented, anchors are clear, ranges do not overlap, and `Other`, `Not applicable`, or `Prefer not to answer` are available where justified.
|
|
11
|
+
5. **Order and burden.** Look for a logical progression, useful grouping, adequate transitions, and placement effects. Estimate burden from length, repetition, open-text effort, matrix density, and branching—not item count alone.
|
|
12
|
+
6. **Routing and requiredness.** Verify that skip logic and validation match eligibility and that required questions do not force fabricated answers. Check whether respondents can recover from an accidental choice.
|
|
13
|
+
7. **Sensitivity, privacy, and inclusion.** Ask only for data that is necessary. Review respectful phrasing, answer granularity, voluntariness, confidentiality context, accessibility, and risks to vulnerable groups.
|
|
14
|
+
8. **Localization.** Evaluate conceptual and cultural equivalence, not only literal completeness. Recommend expert review, back-translation or team reconciliation, and testing with target-language respondents where stakes warrant it.
|
|
15
|
+
9. **Testing and evidence.** Recommend expert review, cognitive interviewing, usability/accessibility testing, a pilot, and analysis of missingness, timing, distributions, reliability, and validity as appropriate to the claimed use.
|
|
16
|
+
|
|
17
|
+
## How to report feedback
|
|
18
|
+
|
|
19
|
+
- State assumptions about purpose, audience, mode, and locale.
|
|
20
|
+
- Prioritize findings by likely effect on measurement quality or respondent harm.
|
|
21
|
+
- Quote or identify the exact item, explain the risk, and suggest a concrete revision.
|
|
22
|
+
- Distinguish defects from choices that require domain or ethics input.
|
|
23
|
+
- Avoid claiming that wording alone makes an instrument scientifically validated.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { I as surveyAssistantSnapshotPayloadSchema } from "./protocol-DV0ka9WT.mjs";
|
|
2
|
+
import { Q as createSurveyAssistantEngineContext } from "./engine-sPvuixWg.mjs";
|
|
3
|
+
import { defaultSurveyAssistantCapabilitySet } from "./capabilities-default.mjs";
|
|
4
|
+
import { MASTRA_RESOURCE_ID_KEY, MASTRA_THREAD_ID_KEY, RequestContext } from "@mastra/core/request-context";
|
|
5
|
+
//#region src/server/request-context.ts
|
|
6
|
+
const parseSurveyAssistantSnapshot = (value) => {
|
|
7
|
+
if (value === void 0 || value === null) return null;
|
|
8
|
+
return surveyAssistantSnapshotPayloadSchema.parse(value);
|
|
9
|
+
};
|
|
10
|
+
const buildSurveyAssistantRequestContext = ({ changeCollector, identity, snapshot, snapshotSource = snapshot ? "full" : "missing", trustedCapabilities = defaultSurveyAssistantCapabilitySet, attachmentAccess, modelAttachments = [], modelSelection }) => {
|
|
11
|
+
const requestContext = new RequestContext();
|
|
12
|
+
requestContext.set("invocation", { kind: "chat" });
|
|
13
|
+
requestContext.set("turnId", changeCollector.turnId);
|
|
14
|
+
requestContext.set("changeCollector", changeCollector);
|
|
15
|
+
requestContext.set("identity", identity);
|
|
16
|
+
if (identity.authority === "authenticated-host") {
|
|
17
|
+
requestContext.set("authenticatedResourceId", identity.resourceId);
|
|
18
|
+
requestContext.set("authorizedThreadId", identity.threadId);
|
|
19
|
+
}
|
|
20
|
+
requestContext.set(MASTRA_RESOURCE_ID_KEY, identity.resourceId);
|
|
21
|
+
requestContext.set(MASTRA_THREAD_ID_KEY, identity.threadId);
|
|
22
|
+
requestContext.set("snapshotSource", snapshotSource);
|
|
23
|
+
if (attachmentAccess && attachmentAccess.documents.length > 0) requestContext.set("attachmentAccess", attachmentAccess);
|
|
24
|
+
if (modelAttachments.length > 0) requestContext.set("modelAttachments", modelAttachments);
|
|
25
|
+
if (modelSelection) requestContext.set("modelSelection", modelSelection);
|
|
26
|
+
if (!snapshot) return requestContext;
|
|
27
|
+
const engineContext = createSurveyAssistantEngineContext(snapshot, trustedCapabilities);
|
|
28
|
+
requestContext.set("documentSnapshot", snapshot);
|
|
29
|
+
requestContext.set("engineContext", engineContext);
|
|
30
|
+
return requestContext;
|
|
31
|
+
};
|
|
32
|
+
const requireSurveyAssistantTurnState = (requestContext) => {
|
|
33
|
+
const turnId = requestContext?.get("turnId");
|
|
34
|
+
const changeCollector = requestContext?.get("changeCollector");
|
|
35
|
+
if (!turnId || !changeCollector || changeCollector.turnId !== turnId) throw new Error("Survey assistant turn context is unavailable or inconsistent.");
|
|
36
|
+
return {
|
|
37
|
+
turnId,
|
|
38
|
+
changeCollector
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
//#endregion
|
|
42
|
+
export { parseSurveyAssistantSnapshot as n, requireSurveyAssistantTurnState as r, buildSurveyAssistantRequestContext as t };
|
|
43
|
+
|
|
44
|
+
//# sourceMappingURL=request-context-jcmXT_Q2.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request-context-jcmXT_Q2.mjs","names":[],"sources":["../src/server/request-context.ts"],"sourcesContent":["import {\n MASTRA_RESOURCE_ID_KEY,\n MASTRA_THREAD_ID_KEY,\n RequestContext,\n} from \"@mastra/core/request-context\";\nimport { surveyAssistantSnapshotPayloadSchema, type SurveyDocumentSnapshot } from \"../protocol\";\nimport {\n createSurveyAssistantEngineContext,\n TurnChangeCollector,\n type SurveyAssistantCapabilitySet,\n type SurveyAssistantEngineContext,\n} from \"../engine\";\nimport { defaultSurveyAssistantCapabilitySet } from \"../capabilities/default\";\nimport type {\n SurveyAssistantAttachmentAccess,\n SurveyAssistantModelAttachment,\n} from \"./thread-documents\";\n\nexport interface SurveyAssistantInvocationContext {\n kind: \"chat\";\n}\n\nexport interface SurveyAssistantIdentityContext {\n authority: \"experimental-client-request\" | \"authenticated-host\";\n resourceId: string;\n threadId: string;\n}\n\nexport interface SurveyAssistantModelSelection {\n id: string;\n model: string;\n}\n\nexport type SurveyAssistantSnapshotSource = \"full\" | \"missing\";\n\nexport interface SurveyAssistantRequestContext {\n invocation: SurveyAssistantInvocationContext;\n turnId: string;\n changeCollector: TurnChangeCollector;\n identity: SurveyAssistantIdentityContext;\n authenticatedResourceId?: string;\n authorizedThreadId?: string;\n applyPolicy?: \"auto\" | \"review\" | \"deny\";\n mastra__resourceId?: string;\n mastra__threadId?: string;\n snapshotSource?: SurveyAssistantSnapshotSource;\n engineContext?: SurveyAssistantEngineContext;\n documentSnapshot?: SurveyDocumentSnapshot;\n modelAttachments?: SurveyAssistantModelAttachment[];\n attachmentAccess?: SurveyAssistantAttachmentAccess;\n modelSelection?: SurveyAssistantModelSelection;\n}\n\nexport const parseSurveyAssistantSnapshot = (value: unknown): SurveyDocumentSnapshot | null => {\n if (value === undefined || value === null) {\n return null;\n }\n\n return surveyAssistantSnapshotPayloadSchema.parse(value);\n};\n\nexport const buildSurveyAssistantRequestContext = ({\n changeCollector,\n identity,\n snapshot,\n snapshotSource = snapshot ? \"full\" : \"missing\",\n trustedCapabilities = defaultSurveyAssistantCapabilitySet,\n attachmentAccess,\n modelAttachments = [],\n modelSelection,\n}: {\n changeCollector: TurnChangeCollector;\n identity: SurveyAssistantIdentityContext;\n snapshot: SurveyDocumentSnapshot | null;\n snapshotSource?: SurveyAssistantSnapshotSource;\n trustedCapabilities?: SurveyAssistantCapabilitySet;\n attachmentAccess?: SurveyAssistantAttachmentAccess;\n modelAttachments?: SurveyAssistantModelAttachment[];\n modelSelection?: SurveyAssistantModelSelection;\n}): RequestContext<SurveyAssistantRequestContext> => {\n const requestContext = new RequestContext<SurveyAssistantRequestContext>();\n\n requestContext.set(\"invocation\", { kind: \"chat\" });\n requestContext.set(\"turnId\", changeCollector.turnId);\n requestContext.set(\"changeCollector\", changeCollector);\n requestContext.set(\"identity\", identity);\n if (identity.authority === \"authenticated-host\") {\n requestContext.set(\"authenticatedResourceId\", identity.resourceId);\n requestContext.set(\"authorizedThreadId\", identity.threadId);\n }\n requestContext.set(MASTRA_RESOURCE_ID_KEY, identity.resourceId);\n requestContext.set(MASTRA_THREAD_ID_KEY, identity.threadId);\n requestContext.set(\"snapshotSource\", snapshotSource);\n if (attachmentAccess && attachmentAccess.documents.length > 0) {\n requestContext.set(\"attachmentAccess\", attachmentAccess);\n }\n if (modelAttachments.length > 0) {\n requestContext.set(\"modelAttachments\", modelAttachments);\n }\n if (modelSelection) {\n requestContext.set(\"modelSelection\", modelSelection);\n }\n\n if (!snapshot) {\n return requestContext;\n }\n\n const engineContext = createSurveyAssistantEngineContext(snapshot, trustedCapabilities);\n requestContext.set(\"documentSnapshot\", snapshot);\n requestContext.set(\"engineContext\", engineContext);\n\n return requestContext;\n};\n\nexport const requireSurveyAssistantTurnState = (\n requestContext: RequestContext<SurveyAssistantRequestContext> | undefined,\n) => {\n const turnId = requestContext?.get(\"turnId\");\n const changeCollector = requestContext?.get(\"changeCollector\");\n if (!turnId || !changeCollector || changeCollector.turnId !== turnId) {\n throw new Error(\"Survey assistant turn context is unavailable or inconsistent.\");\n }\n\n return { turnId, changeCollector };\n};\n"],"mappings":";;;;;AAqDA,MAAa,gCAAgC,UAAkD;AAC7F,KAAI,UAAU,KAAA,KAAa,UAAU,KACnC,QAAO;AAGT,QAAO,qCAAqC,MAAM,MAAM;;AAG1D,MAAa,sCAAsC,EACjD,iBACA,UACA,UACA,iBAAiB,WAAW,SAAS,WACrC,sBAAsB,qCACtB,kBACA,mBAAmB,EAAE,EACrB,qBAUmD;CACnD,MAAM,iBAAiB,IAAI,gBAA+C;AAE1E,gBAAe,IAAI,cAAc,EAAE,MAAM,QAAQ,CAAC;AAClD,gBAAe,IAAI,UAAU,gBAAgB,OAAO;AACpD,gBAAe,IAAI,mBAAmB,gBAAgB;AACtD,gBAAe,IAAI,YAAY,SAAS;AACxC,KAAI,SAAS,cAAc,sBAAsB;AAC/C,iBAAe,IAAI,2BAA2B,SAAS,WAAW;AAClE,iBAAe,IAAI,sBAAsB,SAAS,SAAS;;AAE7D,gBAAe,IAAI,wBAAwB,SAAS,WAAW;AAC/D,gBAAe,IAAI,sBAAsB,SAAS,SAAS;AAC3D,gBAAe,IAAI,kBAAkB,eAAe;AACpD,KAAI,oBAAoB,iBAAiB,UAAU,SAAS,EAC1D,gBAAe,IAAI,oBAAoB,iBAAiB;AAE1D,KAAI,iBAAiB,SAAS,EAC5B,gBAAe,IAAI,oBAAoB,iBAAiB;AAE1D,KAAI,eACF,gBAAe,IAAI,kBAAkB,eAAe;AAGtD,KAAI,CAAC,SACH,QAAO;CAGT,MAAM,gBAAgB,mCAAmC,UAAU,oBAAoB;AACvF,gBAAe,IAAI,oBAAoB,SAAS;AAChD,gBAAe,IAAI,iBAAiB,cAAc;AAElD,QAAO;;AAGT,MAAa,mCACX,mBACG;CACH,MAAM,SAAS,gBAAgB,IAAI,SAAS;CAC5C,MAAM,kBAAkB,gBAAgB,IAAI,kBAAkB;AAC9D,KAAI,CAAC,UAAU,CAAC,mBAAmB,gBAAgB,WAAW,OAC5D,OAAM,IAAI,MAAM,gEAAgE;AAGlF,QAAO;EAAE;EAAQ;EAAiB"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
//#region src/server/request-guardrails.ts
|
|
2
|
+
const SURVEY_ASSISTANT_MAX_REQUEST_BYTES = 20 * 1024 * 1024;
|
|
3
|
+
const SURVEY_ASSISTANT_MAX_MESSAGES = 32;
|
|
4
|
+
const SURVEY_ASSISTANT_MAX_ATTACHMENTS = 4;
|
|
5
|
+
const SURVEY_ASSISTANT_MAX_ATTACHMENT_BYTES = 5 * 1024 * 1024;
|
|
6
|
+
const SURVEY_ASSISTANT_MAX_TOTAL_ATTACHMENT_BYTES = 12 * 1024 * 1024;
|
|
7
|
+
const SURVEY_ASSISTANT_MAX_TEXT_PART_CHARS = 64e3;
|
|
8
|
+
const SURVEY_ASSISTANT_ACCEPTED_ATTACHMENT_TYPES = new Set([
|
|
9
|
+
"application/pdf",
|
|
10
|
+
"image/gif",
|
|
11
|
+
"image/jpeg",
|
|
12
|
+
"image/png",
|
|
13
|
+
"image/webp"
|
|
14
|
+
]);
|
|
15
|
+
var SurveyAssistantRequestError = class extends Error {
|
|
16
|
+
status;
|
|
17
|
+
constructor(message, status = 400) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = "SurveyAssistantRequestError";
|
|
20
|
+
this.status = status;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const getDataUrlBytes = (url, expectedMediaType) => {
|
|
24
|
+
const match = /^data:([^;,]+);base64,([A-Za-z0-9+/]*={0,2})$/.exec(url);
|
|
25
|
+
if (!match) throw new SurveyAssistantRequestError("Attachments must be base64 data URLs.");
|
|
26
|
+
if (match[1]?.toLowerCase() !== expectedMediaType.toLowerCase()) throw new SurveyAssistantRequestError("Attachment media type does not match its data URL.");
|
|
27
|
+
const payload = match[2] ?? "";
|
|
28
|
+
const padding = payload.endsWith("==") ? 2 : payload.endsWith("=") ? 1 : 0;
|
|
29
|
+
return Math.max(0, Math.floor(payload.length * 3 / 4) - padding);
|
|
30
|
+
};
|
|
31
|
+
const validateSurveyAssistantMessages = (value) => {
|
|
32
|
+
if (!Array.isArray(value) || value.length === 0) throw new SurveyAssistantRequestError("A survey assistant message is required.");
|
|
33
|
+
if (value.length > 32) throw new SurveyAssistantRequestError("The survey assistant request contains too many messages.");
|
|
34
|
+
if (value.length !== 1 || value[0]?.role !== "user") throw new SurveyAssistantRequestError("The survey assistant request must contain only the current user message.");
|
|
35
|
+
let attachmentCount = 0;
|
|
36
|
+
let totalAttachmentBytes = 0;
|
|
37
|
+
for (const message of value) {
|
|
38
|
+
if (!message || typeof message !== "object" || !Array.isArray(message.parts)) throw new SurveyAssistantRequestError("The survey assistant messages are malformed.");
|
|
39
|
+
for (const part of message.parts) {
|
|
40
|
+
if (!part || typeof part !== "object" || typeof part.type !== "string") throw new SurveyAssistantRequestError("A survey assistant message part is malformed.");
|
|
41
|
+
if (part.type === "text") {
|
|
42
|
+
if (typeof part.text !== "string") throw new SurveyAssistantRequestError("A survey assistant text part is malformed.");
|
|
43
|
+
if (part.text.length > 64e3) throw new SurveyAssistantRequestError("A survey assistant message is too long.");
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (part.type !== "file") throw new SurveyAssistantRequestError("The survey assistant message part is not supported.");
|
|
47
|
+
attachmentCount += 1;
|
|
48
|
+
if (attachmentCount > 4) throw new SurveyAssistantRequestError("The survey assistant request has too many attachments.");
|
|
49
|
+
if (typeof part.mediaType !== "string" || !SURVEY_ASSISTANT_ACCEPTED_ATTACHMENT_TYPES.has(part.mediaType.toLowerCase())) throw new SurveyAssistantRequestError("The survey assistant attachment type is not supported.");
|
|
50
|
+
if (typeof part.url !== "string") throw new SurveyAssistantRequestError("The survey assistant attachment is malformed.");
|
|
51
|
+
const bytes = getDataUrlBytes(part.url, part.mediaType);
|
|
52
|
+
if (bytes > 5242880) throw new SurveyAssistantRequestError("A survey assistant attachment is too large.");
|
|
53
|
+
totalAttachmentBytes += bytes;
|
|
54
|
+
if (totalAttachmentBytes > 12582912) throw new SurveyAssistantRequestError("The survey assistant attachments are too large in total.");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return value;
|
|
58
|
+
};
|
|
59
|
+
const readSurveyAssistantRequestJson = async (request) => {
|
|
60
|
+
const declaredLength = Number(request.headers.get("content-length"));
|
|
61
|
+
if (Number.isFinite(declaredLength) && declaredLength > 20971520) throw new SurveyAssistantRequestError("The survey assistant request is too large.", 413);
|
|
62
|
+
const text = await request.text();
|
|
63
|
+
if (new TextEncoder().encode(text).byteLength > 20971520) throw new SurveyAssistantRequestError("The survey assistant request is too large.", 413);
|
|
64
|
+
try {
|
|
65
|
+
return JSON.parse(text);
|
|
66
|
+
} catch {
|
|
67
|
+
throw new SurveyAssistantRequestError("The survey assistant request is not valid JSON.");
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
//#endregion
|
|
71
|
+
export { SURVEY_ASSISTANT_MAX_REQUEST_BYTES as a, SurveyAssistantRequestError as c, SURVEY_ASSISTANT_MAX_MESSAGES as i, readSurveyAssistantRequestJson as l, SURVEY_ASSISTANT_MAX_ATTACHMENTS as n, SURVEY_ASSISTANT_MAX_TEXT_PART_CHARS as o, SURVEY_ASSISTANT_MAX_ATTACHMENT_BYTES as r, SURVEY_ASSISTANT_MAX_TOTAL_ATTACHMENT_BYTES as s, SURVEY_ASSISTANT_ACCEPTED_ATTACHMENT_TYPES as t, validateSurveyAssistantMessages as u };
|
|
72
|
+
|
|
73
|
+
//# sourceMappingURL=request-guardrails-C59IWnH8.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request-guardrails-C59IWnH8.mjs","names":[],"sources":["../src/server/request-guardrails.ts"],"sourcesContent":["import type { UIMessage } from \"ai\";\n\nexport const SURVEY_ASSISTANT_MAX_REQUEST_BYTES = 20 * 1024 * 1024;\nexport const SURVEY_ASSISTANT_MAX_MESSAGES = 32;\nexport const SURVEY_ASSISTANT_MAX_ATTACHMENTS = 4;\nexport const SURVEY_ASSISTANT_MAX_ATTACHMENT_BYTES = 5 * 1024 * 1024;\nexport const SURVEY_ASSISTANT_MAX_TOTAL_ATTACHMENT_BYTES = 12 * 1024 * 1024;\nexport const SURVEY_ASSISTANT_MAX_TEXT_PART_CHARS = 64_000;\n\nexport const SURVEY_ASSISTANT_ACCEPTED_ATTACHMENT_TYPES = new Set([\n \"application/pdf\",\n \"image/gif\",\n \"image/jpeg\",\n \"image/png\",\n \"image/webp\",\n]);\n\nexport class SurveyAssistantRequestError extends Error {\n readonly status: number;\n\n constructor(message: string, status = 400) {\n super(message);\n this.name = \"SurveyAssistantRequestError\";\n this.status = status;\n }\n}\n\nconst getDataUrlBytes = (url: string, expectedMediaType: string): number => {\n const match = /^data:([^;,]+);base64,([A-Za-z0-9+/]*={0,2})$/.exec(url);\n if (!match) {\n throw new SurveyAssistantRequestError(\"Attachments must be base64 data URLs.\");\n }\n if (match[1]?.toLowerCase() !== expectedMediaType.toLowerCase()) {\n throw new SurveyAssistantRequestError(\"Attachment media type does not match its data URL.\");\n }\n\n const payload = match[2] ?? \"\";\n const padding = payload.endsWith(\"==\") ? 2 : payload.endsWith(\"=\") ? 1 : 0;\n return Math.max(0, Math.floor((payload.length * 3) / 4) - padding);\n};\n\nexport const validateSurveyAssistantMessages = (value: unknown): UIMessage[] => {\n if (!Array.isArray(value) || value.length === 0) {\n throw new SurveyAssistantRequestError(\"A survey assistant message is required.\");\n }\n if (value.length > SURVEY_ASSISTANT_MAX_MESSAGES) {\n throw new SurveyAssistantRequestError(\n \"The survey assistant request contains too many messages.\",\n );\n }\n if (value.length !== 1 || value[0]?.role !== \"user\") {\n throw new SurveyAssistantRequestError(\n \"The survey assistant request must contain only the current user message.\",\n );\n }\n\n let attachmentCount = 0;\n let totalAttachmentBytes = 0;\n for (const message of value) {\n if (!message || typeof message !== \"object\" || !Array.isArray(message.parts)) {\n throw new SurveyAssistantRequestError(\"The survey assistant messages are malformed.\");\n }\n\n for (const part of message.parts) {\n if (!part || typeof part !== \"object\" || typeof part.type !== \"string\") {\n throw new SurveyAssistantRequestError(\"A survey assistant message part is malformed.\");\n }\n if (part.type === \"text\") {\n if (typeof part.text !== \"string\") {\n throw new SurveyAssistantRequestError(\"A survey assistant text part is malformed.\");\n }\n if (part.text.length > SURVEY_ASSISTANT_MAX_TEXT_PART_CHARS) {\n throw new SurveyAssistantRequestError(\"A survey assistant message is too long.\");\n }\n continue;\n }\n if (part.type !== \"file\") {\n throw new SurveyAssistantRequestError(\n \"The survey assistant message part is not supported.\",\n );\n }\n\n attachmentCount += 1;\n if (attachmentCount > SURVEY_ASSISTANT_MAX_ATTACHMENTS) {\n throw new SurveyAssistantRequestError(\n \"The survey assistant request has too many attachments.\",\n );\n }\n if (\n typeof part.mediaType !== \"string\" ||\n !SURVEY_ASSISTANT_ACCEPTED_ATTACHMENT_TYPES.has(part.mediaType.toLowerCase())\n ) {\n throw new SurveyAssistantRequestError(\n \"The survey assistant attachment type is not supported.\",\n );\n }\n if (typeof part.url !== \"string\") {\n throw new SurveyAssistantRequestError(\"The survey assistant attachment is malformed.\");\n }\n\n const bytes = getDataUrlBytes(part.url, part.mediaType);\n if (bytes > SURVEY_ASSISTANT_MAX_ATTACHMENT_BYTES) {\n throw new SurveyAssistantRequestError(\"A survey assistant attachment is too large.\");\n }\n totalAttachmentBytes += bytes;\n if (totalAttachmentBytes > SURVEY_ASSISTANT_MAX_TOTAL_ATTACHMENT_BYTES) {\n throw new SurveyAssistantRequestError(\n \"The survey assistant attachments are too large in total.\",\n );\n }\n }\n }\n\n return value as UIMessage[];\n};\n\nexport const readSurveyAssistantRequestJson = async (request: Request): Promise<unknown> => {\n const declaredLength = Number(request.headers.get(\"content-length\"));\n if (Number.isFinite(declaredLength) && declaredLength > SURVEY_ASSISTANT_MAX_REQUEST_BYTES) {\n throw new SurveyAssistantRequestError(\"The survey assistant request is too large.\", 413);\n }\n\n const text = await request.text();\n if (new TextEncoder().encode(text).byteLength > SURVEY_ASSISTANT_MAX_REQUEST_BYTES) {\n throw new SurveyAssistantRequestError(\"The survey assistant request is too large.\", 413);\n }\n\n try {\n return JSON.parse(text) as unknown;\n } catch {\n throw new SurveyAssistantRequestError(\"The survey assistant request is not valid JSON.\");\n }\n};\n"],"mappings":";AAEA,MAAa,qCAAqC,KAAK,OAAO;AAC9D,MAAa,gCAAgC;AAC7C,MAAa,mCAAmC;AAChD,MAAa,wCAAwC,IAAI,OAAO;AAChE,MAAa,8CAA8C,KAAK,OAAO;AACvE,MAAa,uCAAuC;AAEpD,MAAa,6CAA6C,IAAI,IAAI;CAChE;CACA;CACA;CACA;CACA;CACD,CAAC;AAEF,IAAa,8BAAb,cAAiD,MAAM;CACrD;CAEA,YAAY,SAAiB,SAAS,KAAK;AACzC,QAAM,QAAQ;AACd,OAAK,OAAO;AACZ,OAAK,SAAS;;;AAIlB,MAAM,mBAAmB,KAAa,sBAAsC;CAC1E,MAAM,QAAQ,gDAAgD,KAAK,IAAI;AACvE,KAAI,CAAC,MACH,OAAM,IAAI,4BAA4B,wCAAwC;AAEhF,KAAI,MAAM,IAAI,aAAa,KAAK,kBAAkB,aAAa,CAC7D,OAAM,IAAI,4BAA4B,qDAAqD;CAG7F,MAAM,UAAU,MAAM,MAAM;CAC5B,MAAM,UAAU,QAAQ,SAAS,KAAK,GAAG,IAAI,QAAQ,SAAS,IAAI,GAAG,IAAI;AACzE,QAAO,KAAK,IAAI,GAAG,KAAK,MAAO,QAAQ,SAAS,IAAK,EAAE,GAAG,QAAQ;;AAGpE,MAAa,mCAAmC,UAAgC;AAC9E,KAAI,CAAC,MAAM,QAAQ,MAAM,IAAI,MAAM,WAAW,EAC5C,OAAM,IAAI,4BAA4B,0CAA0C;AAElF,KAAI,MAAM,SAAA,GACR,OAAM,IAAI,4BACR,2DACD;AAEH,KAAI,MAAM,WAAW,KAAK,MAAM,IAAI,SAAS,OAC3C,OAAM,IAAI,4BACR,2EACD;CAGH,IAAI,kBAAkB;CACtB,IAAI,uBAAuB;AAC3B,MAAK,MAAM,WAAW,OAAO;AAC3B,MAAI,CAAC,WAAW,OAAO,YAAY,YAAY,CAAC,MAAM,QAAQ,QAAQ,MAAM,CAC1E,OAAM,IAAI,4BAA4B,+CAA+C;AAGvF,OAAK,MAAM,QAAQ,QAAQ,OAAO;AAChC,OAAI,CAAC,QAAQ,OAAO,SAAS,YAAY,OAAO,KAAK,SAAS,SAC5D,OAAM,IAAI,4BAA4B,gDAAgD;AAExF,OAAI,KAAK,SAAS,QAAQ;AACxB,QAAI,OAAO,KAAK,SAAS,SACvB,OAAM,IAAI,4BAA4B,6CAA6C;AAErF,QAAI,KAAK,KAAK,SAAA,KACZ,OAAM,IAAI,4BAA4B,0CAA0C;AAElF;;AAEF,OAAI,KAAK,SAAS,OAChB,OAAM,IAAI,4BACR,sDACD;AAGH,sBAAmB;AACnB,OAAI,kBAAA,EACF,OAAM,IAAI,4BACR,yDACD;AAEH,OACE,OAAO,KAAK,cAAc,YAC1B,CAAC,2CAA2C,IAAI,KAAK,UAAU,aAAa,CAAC,CAE7E,OAAM,IAAI,4BACR,yDACD;AAEH,OAAI,OAAO,KAAK,QAAQ,SACtB,OAAM,IAAI,4BAA4B,gDAAgD;GAGxF,MAAM,QAAQ,gBAAgB,KAAK,KAAK,KAAK,UAAU;AACvD,OAAI,QAAA,QACF,OAAM,IAAI,4BAA4B,8CAA8C;AAEtF,2BAAwB;AACxB,OAAI,uBAAA,SACF,OAAM,IAAI,4BACR,2DACD;;;AAKP,QAAO;;AAGT,MAAa,iCAAiC,OAAO,YAAuC;CAC1F,MAAM,iBAAiB,OAAO,QAAQ,QAAQ,IAAI,iBAAiB,CAAC;AACpE,KAAI,OAAO,SAAS,eAAe,IAAI,iBAAA,SACrC,OAAM,IAAI,4BAA4B,8CAA8C,IAAI;CAG1F,MAAM,OAAO,MAAM,QAAQ,MAAM;AACjC,KAAI,IAAI,aAAa,CAAC,OAAO,KAAK,CAAC,aAAA,SACjC,OAAM,IAAI,4BAA4B,8CAA8C,IAAI;AAG1F,KAAI;AACF,SAAO,KAAK,MAAM,KAAK;SACjB;AACN,QAAM,IAAI,4BAA4B,kDAAkD"}
|