viatris-exam-cli 1.8.0 → 1.9.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 +16 -4
- package/package.json +1 -1
- package/skills/viatris-exam-cli/SKILL.md +14 -10
- package/skills/viatris-exam-cli/references/cli-reference.md +85 -51
- package/skills/viatris-exam-cli.zip +0 -0
- package/src/cli.js +0 -0
- package/src/command-handlers/knowledge.js +53 -1
- package/src/help.js +3 -0
- package/src/input.js +12 -3
- package/src/metadata.js +25 -6
- package/src/sanitizers/documents.js +134 -25
package/README.md
CHANGED
|
@@ -5,12 +5,19 @@
|
|
|
5
5
|
## 安装
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npm
|
|
8
|
+
npm install -g viatris-exam-cli
|
|
9
9
|
export VIATRIS_API_KEY="replace-with-real-key"
|
|
10
10
|
viatris-exam login
|
|
11
11
|
unset VIATRIS_API_KEY
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
+
开始任务前更新到最新版本:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install -g viatris-exam-cli@latest
|
|
18
|
+
viatris-exam --version
|
|
19
|
+
```
|
|
20
|
+
|
|
14
21
|
默认连接线上环境:
|
|
15
22
|
|
|
16
23
|
```text
|
|
@@ -95,7 +102,7 @@ viatris-exam products update-knowledge-tree "$PRODUCT_ID" \
|
|
|
95
102
|
viatris-exam products delete-knowledge-tree "$PRODUCT_ID"
|
|
96
103
|
```
|
|
97
104
|
|
|
98
|
-
|
|
105
|
+
产品主数据不属于业务知识树层级;详情、整树回写和整树删除均接受产品编码、产品主数据 ID 或历史知识树根节点 ID,推荐使用稳定的产品编码。固定维度为 L1;知识树回写是完整替换操作,必须一次提交全部 15-500 个 L2/L3/L4 动态节点。整树删除会保留产品根和固定维度,但级联删除动态节点及文档并解绑题目,只能在明确要求时执行。
|
|
99
106
|
|
|
100
107
|
### 知识节点与文档
|
|
101
108
|
|
|
@@ -111,6 +118,9 @@ viatris-exam knowledge documents list <node-id>
|
|
|
111
118
|
viatris-exam knowledge documents create <node-id> --input document.json
|
|
112
119
|
viatris-exam knowledge documents upsert <node-id> --input documents.json
|
|
113
120
|
viatris-exam knowledge documents update <document-id> --input changes.json
|
|
121
|
+
viatris-exam knowledge documents get <document-id>
|
|
122
|
+
viatris-exam knowledge documents link <node-id> <document-id> --input reference.json
|
|
123
|
+
viatris-exam knowledge documents unlink <node-id> <document-id>
|
|
114
124
|
viatris-exam knowledge documents delete <document-id>
|
|
115
125
|
```
|
|
116
126
|
|
|
@@ -118,7 +128,9 @@ viatris-exam knowledge documents delete <document-id>
|
|
|
118
128
|
|
|
119
129
|
`/knowledge` CRUD 响应中的 `level` 是物理存储层级(产品根为 1、固定维度为 2);产品生成接口使用业务层级(产品隐藏、固定维度为 L1、动态节点为 L2-L4)。
|
|
120
130
|
|
|
121
|
-
|
|
131
|
+
知识原文以产品级文档保存,L4 节点通过 `documentRefs` 引用文档,并可携带必须为原文逐字子串的 `markedExcerpts`。关联与解除关联只改变节点引用,不删除文档本体。
|
|
132
|
+
|
|
133
|
+
所有文档写入都会在请求发出前从 `title`、`url`、`summary`、`content` 和 `markedExcerpts` 中移除“小荷”“小荷医疗”和“小荷 AI 医生”及其空格变体。新版写接口发送 `title`、`url`、`content` 和适用场景下的 `markedExcerpts`;兼容字段 `summary` 仍执行净化,但不再写入新版接口。该规则覆盖文档创建、批量 upsert、单文档更新、文档关联和整树回写;标题或原文清洗后为空会直接返回校验错误。
|
|
122
134
|
|
|
123
135
|
### 出题任务
|
|
124
136
|
|
|
@@ -130,7 +142,7 @@ viatris-exam gen-tasks submit "$TASK_ID" --input examples/generated-questions.js
|
|
|
130
142
|
|
|
131
143
|
`externalRef` 是题目回写的幂等键。Agent 重试 POST 前应复用原值。
|
|
132
144
|
|
|
133
|
-
产品全节点任务的 `dimension` 为 `all`。应按 `knowledgeContexts` 覆盖每个叶子节点,并在回写题目时携带对应 `knowledgeNodeId
|
|
145
|
+
产品全节点任务的 `dimension` 为 `all`。应按 `knowledgeContexts` 覆盖每个叶子节点,并在回写题目时携带对应 `knowledgeNodeId`。上下文中的证据文档通过 `excerpts` 提供可用于出题的标记片段;未标记时该数组包含完整原文。
|
|
134
146
|
|
|
135
147
|
### 考试记录
|
|
136
148
|
|
package/package.json
CHANGED
|
@@ -1,24 +1,27 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: viatris-exam-cli
|
|
3
3
|
description: Operate the Viatris exam OpenAPI through its agent-friendly CLI. Use for login, products, knowledge, generation tasks, exam records, scoring, exams, questions, or medical search.
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.9.0
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Viatris Exam CLI
|
|
8
8
|
|
|
9
|
-
Use the
|
|
9
|
+
Use the installed CLI for every supported API operation. Do not call the HTTP endpoints directly when the CLI covers the task.
|
|
10
10
|
|
|
11
11
|
## Start
|
|
12
12
|
|
|
13
|
-
1.
|
|
14
|
-
2.
|
|
15
|
-
3.
|
|
16
|
-
4.
|
|
17
|
-
5.
|
|
13
|
+
1. Read the install and update sections in [references/cli-reference.md](references/cli-reference.md).
|
|
14
|
+
2. Install `viatris-exam-cli` from npm when unavailable.
|
|
15
|
+
3. Update to `viatris-exam-cli@latest` at the start of a task so capability instructions match the installed CLI.
|
|
16
|
+
4. Verify with `viatris-exam --version`.
|
|
17
|
+
5. Prefer the saved credential. To initialize or rotate it, set `VIATRIS_API_KEY` and run `viatris-exam login`; never place the key in command arguments, output, or logs.
|
|
18
|
+
6. Use production by default. Set `VIATRIS_API_ENV=development` only when the user requests development or testing.
|
|
19
|
+
7. Run `viatris-exam describe --compact` when capability discovery or exact machine-readable metadata is needed.
|
|
20
|
+
8. Read the relevant command section in the reference before constructing a write payload or handling a nonzero exit code.
|
|
18
21
|
|
|
19
22
|
## Execute
|
|
20
23
|
|
|
21
|
-
- Invoke the
|
|
24
|
+
- Invoke commands through the installed `viatris-exam` binary.
|
|
22
25
|
- Prefer `--input <file>` for existing payloads and stdin for generated payloads.
|
|
23
26
|
- Parse successful `stdout` as JSON.
|
|
24
27
|
- Parse failed `stderr` as JSON and branch on both exit code and `error.type`.
|
|
@@ -30,8 +33,9 @@ Use the repository-local CLI for every supported API operation. Do not call the
|
|
|
30
33
|
- Apply every returned generation rule. Product-scoped rules identify applicable product codes through `products`; global rules return an empty `products` array.
|
|
31
34
|
- Prefer scoring rule V2 for generated questions and scoring results. Use stable point and deduction-rule IDs.
|
|
32
35
|
- Treat product master records as outside business knowledge-tree levels. Fixed dimensions are business L1; generated nodes are L2-L4. Product detail lookup also accepts a historical knowledge-tree root ID.
|
|
33
|
-
-
|
|
34
|
-
-
|
|
36
|
+
- Store full evidence sources as product-level documents and attach them to L4 nodes through document references. Keep `markedExcerpts` as exact substrings of the referenced `content`.
|
|
37
|
+
- Document writes automatically remove 小荷 source-brand labels, including 小荷医疗 and whitespace variants of 小荷 AI 医生, from title, URL, summary, content, and marked excerpts. Never reintroduce these labels after CLI sanitization.
|
|
38
|
+
- Current document write APIs accept `title`, `url`, `content`, and `markedExcerpts` as applicable. The CLI retains summary sanitization for compatibility but drops an input `summary` from current outbound document payloads.
|
|
35
39
|
- Treat knowledge-tree and recursive node deletion as destructive. Inspect the target first and perform them only when the user explicitly requests deletion.
|
|
36
40
|
- Do not automatically retry POST, PUT, PATCH, or DELETE commands. Inspect the returned error and determine whether the server accepted the request.
|
|
37
41
|
- Keep temporary payloads outside the repository unless they are requested deliverables.
|
|
@@ -1,12 +1,30 @@
|
|
|
1
1
|
# CLI Reference
|
|
2
2
|
|
|
3
|
+
## Install
|
|
4
|
+
|
|
5
|
+
Install the CLI globally so every example below can use the `viatris-exam` command directly:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g viatris-exam-cli
|
|
9
|
+
viatris-exam --version
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Update
|
|
13
|
+
|
|
14
|
+
Update before starting a task so the installed capabilities and this Skill remain aligned:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install -g viatris-exam-cli@latest
|
|
18
|
+
viatris-exam --version
|
|
19
|
+
```
|
|
20
|
+
|
|
3
21
|
## Authentication And Environment
|
|
4
22
|
|
|
5
23
|
Login once:
|
|
6
24
|
|
|
7
25
|
```bash
|
|
8
26
|
export VIATRIS_API_KEY="..."
|
|
9
|
-
|
|
27
|
+
viatris-exam login
|
|
10
28
|
unset VIATRIS_API_KEY
|
|
11
29
|
```
|
|
12
30
|
|
|
@@ -42,9 +60,9 @@ export VIATRIS_API_KEY_PREFIX=""
|
|
|
42
60
|
## Capability Discovery
|
|
43
61
|
|
|
44
62
|
```bash
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
63
|
+
viatris-exam describe --compact
|
|
64
|
+
viatris-exam --help
|
|
65
|
+
viatris-exam --version
|
|
48
66
|
```
|
|
49
67
|
|
|
50
68
|
`describe` does not require credentials. It returns commands, paths, environment metadata, authentication metadata, and exit codes as JSON.
|
|
@@ -54,7 +72,7 @@ node src/cli.js --version
|
|
|
54
72
|
List product master data:
|
|
55
73
|
|
|
56
74
|
```bash
|
|
57
|
-
|
|
75
|
+
viatris-exam products list --compact
|
|
58
76
|
```
|
|
59
77
|
|
|
60
78
|
The response is `{ "items": [...] }`, ordered by `sortOrder` and then name. Each record includes the stable `code`, Chinese name, optional English name, generic name, indication, dosage form, `active` or `inactive` status, remark, and timestamps.
|
|
@@ -62,7 +80,7 @@ The response is `{ "items": [...] }`, ordered by `sortOrder` and then name. Each
|
|
|
62
80
|
Create a product and initialize its canonical knowledge-tree root:
|
|
63
81
|
|
|
64
82
|
```bash
|
|
65
|
-
|
|
83
|
+
viatris-exam products create --json '{
|
|
66
84
|
"name": "络活喜",
|
|
67
85
|
"genericName": "苯磺酸氨氯地平片",
|
|
68
86
|
"indication": "高血压、冠心病",
|
|
@@ -77,15 +95,15 @@ Only `name` is required and the Chinese name must be unique. `code` is optional;
|
|
|
77
95
|
Get a product and its fixed L1 dimensions:
|
|
78
96
|
|
|
79
97
|
```bash
|
|
80
|
-
|
|
98
|
+
viatris-exam products get "$PRODUCT_ID" --compact
|
|
81
99
|
```
|
|
82
100
|
|
|
83
|
-
|
|
101
|
+
Product detail, full-tree update, and full-tree deletion accept a stable product code, product master ID, or historical knowledge-tree root ID. Prefer the product code. The response also contains nullable `nameEn`, `genericName`, `indication`, `dosageForm`, and `remark`. Product master data is outside business levels L1-L4. Use each returned `fixedDimensions[].clientRef` as the parent reference for an L2 node. Each fixed dimension also returns `level: 1` and a display `codePrefix`.
|
|
84
102
|
|
|
85
103
|
Update verified optional medical master data:
|
|
86
104
|
|
|
87
105
|
```bash
|
|
88
|
-
|
|
106
|
+
viatris-exam products update "$PRODUCT_ID" --json '{
|
|
89
107
|
"nameEn": "Lipitor",
|
|
90
108
|
"genericName": "阿托伐他汀钙片",
|
|
91
109
|
"indication": "高胆固醇血症、混合型血脂异常及心血管疾病预防",
|
|
@@ -99,7 +117,7 @@ Only supplied fields are updated. Each supplied value must be a nonempty string
|
|
|
99
117
|
Replace a product knowledge tree:
|
|
100
118
|
|
|
101
119
|
```bash
|
|
102
|
-
|
|
120
|
+
viatris-exam products update-knowledge-tree "$PRODUCT_ID" \
|
|
103
121
|
--input knowledge-tree.json \
|
|
104
122
|
--compact
|
|
105
123
|
```
|
|
@@ -108,6 +126,14 @@ Payload shape:
|
|
|
108
126
|
|
|
109
127
|
```json
|
|
110
128
|
{
|
|
129
|
+
"documents": [
|
|
130
|
+
{
|
|
131
|
+
"clientRef": "doc.lipitor_pi",
|
|
132
|
+
"title": "立普妥说明书",
|
|
133
|
+
"url": "https://example.com/evidence/lipitor.pdf",
|
|
134
|
+
"content": "阿托伐他汀每日一次,给药剂量为10~80mg。"
|
|
135
|
+
}
|
|
136
|
+
],
|
|
111
137
|
"nodes": [
|
|
112
138
|
{
|
|
113
139
|
"clientRef": "disease.hypertension",
|
|
@@ -132,10 +158,12 @@ Payload shape:
|
|
|
132
158
|
"dimensionCode": "disease",
|
|
133
159
|
"name": "诊断阈值",
|
|
134
160
|
"sortOrder": 1,
|
|
135
|
-
"
|
|
161
|
+
"documentRefs": [
|
|
136
162
|
{
|
|
137
|
-
"
|
|
138
|
-
"
|
|
163
|
+
"documentRef": "doc.lipitor_pi",
|
|
164
|
+
"markedExcerpts": [
|
|
165
|
+
"阿托伐他汀每日一次,给药剂量为10~80mg"
|
|
166
|
+
]
|
|
139
167
|
}
|
|
140
168
|
]
|
|
141
169
|
}
|
|
@@ -143,12 +171,12 @@ Payload shape:
|
|
|
143
171
|
}
|
|
144
172
|
```
|
|
145
173
|
|
|
146
|
-
Submit the complete L2/L3/L4 node set, between 15 and 500 nodes. Every `clientRef` must be unique within
|
|
174
|
+
Submit up to 200 product-level source documents and the complete L2/L3/L4 node set, between 15 and 500 nodes. Every document and node `clientRef` must be unique within its collection. L2 nodes use `dimension:<code>` as `parentClientRef`; L3/L4 nodes use their direct parent's `clientRef`. Only L4 nodes may use `documentRefs`, every L4 node needs at least one reference, and every marked excerpt must be an exact substring of the referenced document content. A successful PUT replaces the previous dynamic nodes, so do not send a partial tree.
|
|
147
175
|
|
|
148
176
|
Delete all dynamic knowledge-tree content:
|
|
149
177
|
|
|
150
178
|
```bash
|
|
151
|
-
|
|
179
|
+
viatris-exam products delete-knowledge-tree "$PRODUCT_ID"
|
|
152
180
|
```
|
|
153
181
|
|
|
154
182
|
This destructive operation preserves the product root and five fixed L1 dimensions, deletes all L2-L4 nodes and evidence documents, and unbinds affected questions. Inspect the product and tree first, and execute only when the user explicitly requests the full reset.
|
|
@@ -158,7 +186,7 @@ This destructive operation preserves the product root and five fixed L1 dimensio
|
|
|
158
186
|
Get the complete physical tree and all product documents:
|
|
159
187
|
|
|
160
188
|
```bash
|
|
161
|
-
|
|
189
|
+
viatris-exam knowledge tree lipitor --compact
|
|
162
190
|
```
|
|
163
191
|
|
|
164
192
|
The incremental `/knowledge` responses expose physical storage levels where the product root is 1 and fixed dimensions are 2. This differs from the product-generation business taxonomy, where the product is hidden, fixed dimensions are L1, and generated content is L2-L4.
|
|
@@ -166,7 +194,7 @@ The incremental `/knowledge` responses expose physical storage levels where the
|
|
|
166
194
|
Create or reuse a visible path:
|
|
167
195
|
|
|
168
196
|
```bash
|
|
169
|
-
|
|
197
|
+
viatris-exam knowledge nodes by-path --json \
|
|
170
198
|
'{"product":"lipitor","path":["疾病知识","血脂异常基础","ASCVD 风险评估"]}'
|
|
171
199
|
```
|
|
172
200
|
|
|
@@ -175,20 +203,20 @@ The path has one to three entries. The first entry must be a fixed dimension nam
|
|
|
175
203
|
Create or reuse one node under a known parent:
|
|
176
204
|
|
|
177
205
|
```bash
|
|
178
|
-
|
|
206
|
+
viatris-exam knowledge nodes create --json \
|
|
179
207
|
'{"parentId":"PARENT_ID","name":"ASCVD 风险评估","sortOrder":20}'
|
|
180
208
|
```
|
|
181
209
|
|
|
182
210
|
Read, update, move, or delete a node:
|
|
183
211
|
|
|
184
212
|
```bash
|
|
185
|
-
|
|
186
|
-
|
|
213
|
+
viatris-exam knowledge nodes get "$NODE_ID"
|
|
214
|
+
viatris-exam knowledge nodes update "$NODE_ID" \
|
|
187
215
|
--json '{"name":"血脂异常与他汀治疗","sortOrder":5}'
|
|
188
|
-
|
|
216
|
+
viatris-exam knowledge nodes move "$NODE_ID" \
|
|
189
217
|
--json '{"newParentId":"NEW_PARENT_ID","sortOrder":30}'
|
|
190
|
-
|
|
191
|
-
|
|
218
|
+
viatris-exam knowledge nodes delete "$NODE_ID"
|
|
219
|
+
viatris-exam knowledge nodes delete "$NODE_ID" --recursive
|
|
192
220
|
```
|
|
193
221
|
|
|
194
222
|
Product roots and fixed dimensions are protected from rename, move, and deletion. Moving cannot cross products, target the node itself or its descendants, or produce levels deeper than L4. Plain deletion is the safe default and returns HTTP 409 when children or question bindings exist. `--recursive` deletes the full subtree and documents and unbinds affected questions. Inspect the node first and use recursive deletion only when explicitly requested.
|
|
@@ -196,21 +224,27 @@ Product roots and fixed dimensions are protected from rename, move, and deletion
|
|
|
196
224
|
Manage documents:
|
|
197
225
|
|
|
198
226
|
```bash
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
--json '{"title":"说明书","
|
|
202
|
-
|
|
203
|
-
--json '{"items":[{"title":"说明书","
|
|
204
|
-
|
|
205
|
-
--json '{"
|
|
206
|
-
|
|
227
|
+
viatris-exam knowledge documents list "$NODE_ID"
|
|
228
|
+
viatris-exam knowledge documents create "$NODE_ID" \
|
|
229
|
+
--json '{"title":"说明书","url":"https://example.com/pi.pdf","content":"完整原文","markedExcerpts":["完整原文"]}'
|
|
230
|
+
viatris-exam knowledge documents upsert "$NODE_ID" \
|
|
231
|
+
--json '{"items":[{"title":"说明书","content":"更新后的完整原文","markedExcerpts":["完整原文"]}]}'
|
|
232
|
+
viatris-exam knowledge documents update "$DOCUMENT_ID" \
|
|
233
|
+
--json '{"content":"更新后的完整原文"}'
|
|
234
|
+
viatris-exam knowledge documents get "$DOCUMENT_ID"
|
|
235
|
+
viatris-exam knowledge documents link "$NODE_ID" "$DOCUMENT_ID" \
|
|
236
|
+
--json '{"markedExcerpts":["更新后的完整原文"]}'
|
|
237
|
+
viatris-exam knowledge documents unlink "$NODE_ID" "$DOCUMENT_ID"
|
|
238
|
+
viatris-exam knowledge documents delete "$DOCUMENT_ID"
|
|
207
239
|
```
|
|
208
240
|
|
|
209
|
-
Create reuses a document with the same title
|
|
241
|
+
Documents are product-level source records containing full `content`; nodes hold references to them. Create requires `title` and `content`, reuses a document with the same title, and attaches it to the node. Batch upsert accepts 1-20 items. For an existing title, omitted `content` keeps the stored source, omitted `markedExcerpts` keeps the reference marks, and an empty `markedExcerpts` array clears them. A single-document update changes only supplied `title`, `url`, or `content`; send `url: null` to clear the URL.
|
|
242
|
+
|
|
243
|
+
`get` returns the full source and every referencing node. `link` attaches an existing document from the same product; its request body is optional. Supplying `markedExcerpts` replaces the existing marks, an empty array clears them, and omission preserves them. `unlink` deletes only the node reference. Inspect references before an explicitly requested document delete.
|
|
210
244
|
|
|
211
245
|
### Document Sanitization
|
|
212
246
|
|
|
213
|
-
Before every document write, the CLI removes source-brand labels from `title`, `url`, and `
|
|
247
|
+
Before every document write, the CLI removes source-brand labels from `title`, `url`, `summary`, `content`, and `markedExcerpts`. This applies to document create, batch upsert, single-document update, node links, and product-level documents plus L4 references in `products update-knowledge-tree`. The compatibility `summary` field remains sanitized but is dropped from current outbound document payloads.
|
|
214
248
|
|
|
215
249
|
Removed forms include:
|
|
216
250
|
|
|
@@ -218,12 +252,12 @@ Removed forms include:
|
|
|
218
252
|
- `小荷医疗`, including whitespace between `小荷` and `医疗`
|
|
219
253
|
- `小荷AI医生` with arbitrary whitespace, including `小荷 AI 医生`, `小荷 AI医生`, and `小荷AI 医生`
|
|
220
254
|
|
|
221
|
-
Whitespace is normalized after removal, and leading or trailing title separators are removed. If a supplied title becomes empty, the CLI returns `validation_error` without sending the request.
|
|
255
|
+
Whitespace is normalized after removal, and leading or trailing title separators are removed. If a supplied title, content, or marked excerpt becomes empty, the CLI returns `validation_error` without sending the request. When source content and excerpts are supplied together, the CLI also verifies every sanitized excerpt remains an exact substring of the sanitized content.
|
|
222
256
|
|
|
223
257
|
List generation tasks:
|
|
224
258
|
|
|
225
259
|
```bash
|
|
226
|
-
|
|
260
|
+
viatris-exam gen-tasks list \
|
|
227
261
|
--status generating \
|
|
228
262
|
--limit 20 \
|
|
229
263
|
--compact
|
|
@@ -234,17 +268,17 @@ node src/cli.js gen-tasks list \
|
|
|
234
268
|
Get one generation task:
|
|
235
269
|
|
|
236
270
|
```bash
|
|
237
|
-
|
|
271
|
+
viatris-exam gen-tasks get "$TASK_ID" --compact
|
|
238
272
|
```
|
|
239
273
|
|
|
240
|
-
For a leaf-node task, `knowledgeContext` is the mandatory target context. For a product-wide task, `dimension` is `all`, `knowledgeContext` is null, and `knowledgeContexts` contains every leaf context. Generate `questionsPerNode` questions for every listed context and follow all returned `rules`.
|
|
274
|
+
For a leaf-node task, `knowledgeContext` is the mandatory target context. For a product-wide task, `dimension` is `all`, `knowledgeContext` is null, and `knowledgeContexts` contains every leaf context. Generate `questionsPerNode` questions for every listed context and follow all returned `rules`. Each context document exposes `excerpts`; these are the marked source substrings when marks exist, otherwise the array contains the full source content.
|
|
241
275
|
|
|
242
276
|
Each rule has `scope` and `products`. For `scope: "product"`, `products` contains one or more applicable product codes. For `scope: "global"`, `products` is an empty array. Do not read the removed single-value `product` field.
|
|
243
277
|
|
|
244
278
|
Submit generated questions:
|
|
245
279
|
|
|
246
280
|
```bash
|
|
247
|
-
|
|
281
|
+
viatris-exam gen-tasks submit "$TASK_ID" --input payload.json --compact
|
|
248
282
|
```
|
|
249
283
|
|
|
250
284
|
Payload shape:
|
|
@@ -315,13 +349,13 @@ Omit `knowledgeNodeId` for ordinary tasks. For a product-wide task it is require
|
|
|
315
349
|
Get a submitted exam record and its frozen answers:
|
|
316
350
|
|
|
317
351
|
```bash
|
|
318
|
-
|
|
352
|
+
viatris-exam exam-records get "$RECORD_ID" --compact
|
|
319
353
|
```
|
|
320
354
|
|
|
321
355
|
Get the complete scoring rules bound to the record:
|
|
322
356
|
|
|
323
357
|
```bash
|
|
324
|
-
|
|
358
|
+
viatris-exam exam-records scoring-rules "$RECORD_ID" --compact
|
|
325
359
|
```
|
|
326
360
|
|
|
327
361
|
The response includes the scoring `taskId`, `schemaVersion`, frozen question versions, evidence, V2 deduction rules, score caps, and item IDs. Use its `taskId` for scoring submission.
|
|
@@ -329,13 +363,13 @@ The response includes the scoring `taskId`, `schemaVersion`, frozen question ver
|
|
|
329
363
|
Get a scoring task:
|
|
330
364
|
|
|
331
365
|
```bash
|
|
332
|
-
|
|
366
|
+
viatris-exam scoring get "$TASK_ID" --compact
|
|
333
367
|
```
|
|
334
368
|
|
|
335
369
|
Submit scoring results:
|
|
336
370
|
|
|
337
371
|
```bash
|
|
338
|
-
|
|
372
|
+
viatris-exam scoring submit "$TASK_ID" --input results.json --compact
|
|
339
373
|
```
|
|
340
374
|
|
|
341
375
|
Payload shape:
|
|
@@ -367,7 +401,7 @@ For V2, submit exactly one `pointJudgement` per scoring point. IDs must match th
|
|
|
367
401
|
Create an exam:
|
|
368
402
|
|
|
369
403
|
```bash
|
|
370
|
-
|
|
404
|
+
viatris-exam exams create --input exam.json --compact
|
|
371
405
|
```
|
|
372
406
|
|
|
373
407
|
Required top-level fields are `name`, `product`, `questionCount`, `startAt`, `endAt`, `resultPolicy`, and `assigneeIds`. `questionCount` is 1 through 150. Use either `questions` or legacy `questionIds`, never both. Each question score must be a positive integer and the complete exam must total exactly 100.
|
|
@@ -375,7 +409,7 @@ Required top-level fields are `name`, `product`, `questionCount`, `startAt`, `en
|
|
|
375
409
|
Create questions directly:
|
|
376
410
|
|
|
377
411
|
```bash
|
|
378
|
-
|
|
412
|
+
viatris-exam questions create --input questions.json --compact
|
|
379
413
|
```
|
|
380
414
|
|
|
381
415
|
Payload shape is `{ "items": [...] }`. Each item requires `product`, `dimension`, and `content`; provide `externalRef` for idempotency. Prefer `scoringRuleVersion: 2`, stable IDs on scoring points and deduction rules, and explicit `scoreCapRules`.
|
|
@@ -385,7 +419,7 @@ The API has partial-success semantics: inspect `accepted`, `duplicates`, and eve
|
|
|
385
419
|
Add a question version:
|
|
386
420
|
|
|
387
421
|
```bash
|
|
388
|
-
|
|
422
|
+
viatris-exam questions version "$QUESTION_ID" --input version.json --compact
|
|
389
423
|
```
|
|
390
424
|
|
|
391
425
|
Required fields are `stem`, `referenceAnswer`, nonempty `scoringPoints`, `evidence`, and `editReason`. V2 versions may also include `scoringRuleVersion: 2`, deduction rules, and `scoreCapRules`.
|
|
@@ -393,7 +427,7 @@ Required fields are `stem`, `referenceAnswer`, nonempty `scoringPoints`, `eviden
|
|
|
393
427
|
Search medical evidence:
|
|
394
428
|
|
|
395
429
|
```bash
|
|
396
|
-
|
|
430
|
+
viatris-exam medical-search search "阿托伐他汀 适应症" \
|
|
397
431
|
--file-type drug \
|
|
398
432
|
--limit 20 \
|
|
399
433
|
--max-length 1200 \
|
|
@@ -404,7 +438,7 @@ Or use JSON:
|
|
|
404
438
|
|
|
405
439
|
```bash
|
|
406
440
|
printf '%s' '{"query":"阿托伐他汀 适应症","limit":20}' |
|
|
407
|
-
|
|
441
|
+
viatris-exam medical-search search --compact
|
|
408
442
|
```
|
|
409
443
|
|
|
410
444
|
Do not combine query flags with JSON input. `limit` accepts 1-20 and defaults to 20 when omitted. `maxLength` defaults to 1200.
|
|
@@ -416,10 +450,10 @@ The response `rawJson` is already a structured object. Read `rawJson.documents`
|
|
|
416
450
|
Choose exactly one input form:
|
|
417
451
|
|
|
418
452
|
```bash
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
printf '%s' '{"items":[]}' |
|
|
453
|
+
viatris-exam questions create --input payload.json
|
|
454
|
+
viatris-exam questions create --input - < payload.json
|
|
455
|
+
viatris-exam questions create --json '{"items":[]}'
|
|
456
|
+
printf '%s' '{"items":[]}' | viatris-exam questions create
|
|
423
457
|
```
|
|
424
458
|
|
|
425
459
|
The top-level JSON value must be an object.
|
|
Binary file
|
package/src/cli.js
CHANGED
|
File without changes
|
|
@@ -3,10 +3,11 @@ import {
|
|
|
3
3
|
assertOptions,
|
|
4
4
|
requiredId,
|
|
5
5
|
} from "../args.js";
|
|
6
|
-
import { readJsonInput } from "../input.js";
|
|
6
|
+
import { readJsonInput, readOptionalJsonInput } from "../input.js";
|
|
7
7
|
import {
|
|
8
8
|
sanitizeDocumentBatchPayload,
|
|
9
9
|
sanitizeDocumentPayload,
|
|
10
|
+
sanitizeDocumentReferencePayload,
|
|
10
11
|
} from "../sanitizers/documents.js";
|
|
11
12
|
|
|
12
13
|
async function jsonCommand(
|
|
@@ -27,6 +28,16 @@ function nodePath(positionals, label) {
|
|
|
27
28
|
return requiredId(positionals, 3, label);
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
async function optionalJsonCommand(method, path, options, transform) {
|
|
32
|
+
assertOptions(options, ["input", "json"]);
|
|
33
|
+
const body = await readOptionalJsonInput(options);
|
|
34
|
+
return {
|
|
35
|
+
method,
|
|
36
|
+
path,
|
|
37
|
+
...(body === undefined ? {} : { body: transform(body) }),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
30
41
|
export async function resolveKnowledge(positionals, options) {
|
|
31
42
|
const [resource, area, action] = positionals;
|
|
32
43
|
if (resource !== "knowledge") return null;
|
|
@@ -135,6 +146,47 @@ export async function resolveKnowledge(positionals, options) {
|
|
|
135
146
|
);
|
|
136
147
|
}
|
|
137
148
|
|
|
149
|
+
if (area === "documents" && action === "get") {
|
|
150
|
+
assertNoExtraPositionals(positionals, 4);
|
|
151
|
+
assertOptions(options);
|
|
152
|
+
const id = nodePath(positionals, "knowledge document ID");
|
|
153
|
+
return {
|
|
154
|
+
method: "GET",
|
|
155
|
+
path: `/openapi/knowledge/documents/${id}`,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (area === "documents" && action === "link") {
|
|
160
|
+
assertNoExtraPositionals(positionals, 5);
|
|
161
|
+
const nodeId = nodePath(positionals, "knowledge node ID");
|
|
162
|
+
const documentId = requiredId(
|
|
163
|
+
positionals,
|
|
164
|
+
4,
|
|
165
|
+
"knowledge document ID",
|
|
166
|
+
);
|
|
167
|
+
return optionalJsonCommand(
|
|
168
|
+
"PUT",
|
|
169
|
+
`/openapi/knowledge/nodes/${nodeId}/documents/${documentId}`,
|
|
170
|
+
options,
|
|
171
|
+
sanitizeDocumentReferencePayload,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (area === "documents" && action === "unlink") {
|
|
176
|
+
assertNoExtraPositionals(positionals, 5);
|
|
177
|
+
assertOptions(options);
|
|
178
|
+
const nodeId = nodePath(positionals, "knowledge node ID");
|
|
179
|
+
const documentId = requiredId(
|
|
180
|
+
positionals,
|
|
181
|
+
4,
|
|
182
|
+
"knowledge document ID",
|
|
183
|
+
);
|
|
184
|
+
return {
|
|
185
|
+
method: "DELETE",
|
|
186
|
+
path: `/openapi/knowledge/nodes/${nodeId}/documents/${documentId}`,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
138
190
|
if (area === "documents" && action === "delete") {
|
|
139
191
|
assertNoExtraPositionals(positionals, 4);
|
|
140
192
|
assertOptions(options);
|
package/src/help.js
CHANGED
|
@@ -27,6 +27,9 @@ Commands:
|
|
|
27
27
|
knowledge documents create <node-id> --input <file|->
|
|
28
28
|
knowledge documents upsert <node-id> --input <file|->
|
|
29
29
|
knowledge documents update <document-id> --input <file|->
|
|
30
|
+
knowledge documents get <document-id>
|
|
31
|
+
knowledge documents link <node-id> <document-id> [--input <file|->]
|
|
32
|
+
knowledge documents unlink <node-id> <document-id>
|
|
30
33
|
knowledge documents delete <document-id>
|
|
31
34
|
gen-tasks list [--status STATUS] [--limit N]
|
|
32
35
|
gen-tasks get <task-id>
|
package/src/input.js
CHANGED
|
@@ -11,8 +11,7 @@ export function hasPipedInput() {
|
|
|
11
11
|
return !process.stdin.isTTY;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
const canReadStdin = hasPipedInput();
|
|
14
|
+
async function readJsonSource(options, required) {
|
|
16
15
|
if (options.input !== undefined && options.json !== undefined) {
|
|
17
16
|
throw new CliError("usage_error", "Use only one of --input and --json.");
|
|
18
17
|
}
|
|
@@ -25,10 +24,11 @@ export async function readJsonInput(options) {
|
|
|
25
24
|
} else if (options.json !== undefined) {
|
|
26
25
|
source = "--json";
|
|
27
26
|
text = options.json;
|
|
28
|
-
} else if (
|
|
27
|
+
} else if (hasPipedInput()) {
|
|
29
28
|
source = "stdin";
|
|
30
29
|
text = await readStdin();
|
|
31
30
|
} else {
|
|
31
|
+
if (!required) return undefined;
|
|
32
32
|
throw new CliError(
|
|
33
33
|
"usage_error",
|
|
34
34
|
"JSON input is required. Use --input <file>, --input -, --json <value>, or stdin.",
|
|
@@ -36,6 +36,7 @@ export async function readJsonInput(options) {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
if (!text.trim()) {
|
|
39
|
+
if (!required && source === "stdin") return undefined;
|
|
39
40
|
throw new CliError("input_error", `JSON input from ${source} is empty.`);
|
|
40
41
|
}
|
|
41
42
|
try {
|
|
@@ -51,3 +52,11 @@ export async function readJsonInput(options) {
|
|
|
51
52
|
);
|
|
52
53
|
}
|
|
53
54
|
}
|
|
55
|
+
|
|
56
|
+
export async function readJsonInput(options) {
|
|
57
|
+
return readJsonSource(options, true);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function readOptionalJsonInput(options) {
|
|
61
|
+
return readJsonSource(options, false);
|
|
62
|
+
}
|
package/src/metadata.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CREDENTIAL_FILE_DISPLAY } from "./credentials.js";
|
|
2
2
|
import { DOCUMENT_SANITIZATION_POLICY } from "./sanitizers/documents.js";
|
|
3
3
|
|
|
4
|
-
export const VERSION = "1.
|
|
4
|
+
export const VERSION = "1.9.0";
|
|
5
5
|
export const DEFAULT_TIMEOUT_MS = 30_000;
|
|
6
6
|
export const DEFAULT_RETRIES = 2;
|
|
7
7
|
|
|
@@ -54,8 +54,8 @@ export const COMMANDS = Object.freeze([
|
|
|
54
54
|
command: "products update-knowledge-tree <product-id>",
|
|
55
55
|
method: "PUT",
|
|
56
56
|
path: "/openapi/products/{id}/knowledge-tree",
|
|
57
|
-
input: "{nodes: [...]}",
|
|
58
|
-
summary: "Replace
|
|
57
|
+
input: "{documents?: [...], nodes: [...documentRefs]}",
|
|
58
|
+
summary: "Replace product documents and dynamic L2/L3/L4 nodes.",
|
|
59
59
|
},
|
|
60
60
|
{
|
|
61
61
|
command: "products delete-knowledge-tree <product-id>",
|
|
@@ -122,8 +122,8 @@ export const COMMANDS = Object.freeze([
|
|
|
122
122
|
command: "knowledge documents create <node-id>",
|
|
123
123
|
method: "POST",
|
|
124
124
|
path: "/openapi/knowledge/nodes/{id}/documents",
|
|
125
|
-
input: "{title,
|
|
126
|
-
summary: "
|
|
125
|
+
input: "{title, content, url?, markedExcerpts?}",
|
|
126
|
+
summary: "Create or reuse a product document and attach it to a node.",
|
|
127
127
|
},
|
|
128
128
|
{
|
|
129
129
|
command: "knowledge documents upsert <node-id>",
|
|
@@ -136,9 +136,28 @@ export const COMMANDS = Object.freeze([
|
|
|
136
136
|
command: "knowledge documents update <document-id>",
|
|
137
137
|
method: "PUT",
|
|
138
138
|
path: "/openapi/knowledge/documents/{id}",
|
|
139
|
-
input: "{title?,
|
|
139
|
+
input: "{title?, url?, content?}",
|
|
140
140
|
summary: "Update supplied fields on a knowledge document.",
|
|
141
141
|
},
|
|
142
|
+
{
|
|
143
|
+
command: "knowledge documents get <document-id>",
|
|
144
|
+
method: "GET",
|
|
145
|
+
path: "/openapi/knowledge/documents/{id}",
|
|
146
|
+
summary: "Get document source content and all referencing nodes.",
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
command: "knowledge documents link <node-id> <document-id>",
|
|
150
|
+
method: "PUT",
|
|
151
|
+
path: "/openapi/knowledge/nodes/{id}/documents/{documentId}",
|
|
152
|
+
input: "{markedExcerpts?} (optional)",
|
|
153
|
+
summary: "Attach an existing product document or update its excerpts.",
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
command: "knowledge documents unlink <node-id> <document-id>",
|
|
157
|
+
method: "DELETE",
|
|
158
|
+
path: "/openapi/knowledge/nodes/{id}/documents/{documentId}",
|
|
159
|
+
summary: "Remove a node reference without deleting the document.",
|
|
160
|
+
},
|
|
142
161
|
{
|
|
143
162
|
command: "knowledge documents delete <document-id>",
|
|
144
163
|
method: "DELETE",
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { CliError } from "../errors.js";
|
|
2
2
|
|
|
3
3
|
export const DOCUMENT_SANITIZATION_POLICY = Object.freeze({
|
|
4
|
-
fields: ["title", "url", "summary"],
|
|
5
|
-
writableFields: ["title", "
|
|
6
|
-
droppedWriteFields: ["
|
|
4
|
+
fields: ["title", "url", "summary", "content", "markedExcerpts"],
|
|
5
|
+
writableFields: ["title", "url", "content", "markedExcerpts"],
|
|
6
|
+
droppedWriteFields: ["summary"],
|
|
7
7
|
removedLabels: [
|
|
8
8
|
"小荷",
|
|
9
9
|
"小荷医疗",
|
|
@@ -14,8 +14,10 @@ export const DOCUMENT_SANITIZATION_POLICY = Object.freeze({
|
|
|
14
14
|
"knowledge documents create",
|
|
15
15
|
"knowledge documents upsert",
|
|
16
16
|
"knowledge documents update",
|
|
17
|
+
"knowledge documents link",
|
|
17
18
|
],
|
|
18
19
|
emptyTitle: "validation_error",
|
|
20
|
+
emptyContent: "validation_error",
|
|
19
21
|
});
|
|
20
22
|
|
|
21
23
|
const PROHIBITED_LABEL = /小荷\s*(?:AI\s*医生|医疗)?/giu;
|
|
@@ -51,14 +53,57 @@ export function sanitizeDocument(document, location = "document") {
|
|
|
51
53
|
);
|
|
52
54
|
}
|
|
53
55
|
}
|
|
54
|
-
for (const field of ["url", "summary"]) {
|
|
56
|
+
for (const field of ["url", "summary", "content"]) {
|
|
55
57
|
if (typeof sanitized[field] === "string") {
|
|
56
58
|
sanitized[field] = sanitizeText(sanitized[field]);
|
|
57
59
|
}
|
|
58
60
|
}
|
|
61
|
+
if (typeof sanitized.content === "string" && !sanitized.content) {
|
|
62
|
+
throw new CliError(
|
|
63
|
+
"validation_error",
|
|
64
|
+
`${location}.content is empty after prohibited source labels are removed.`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
if (Array.isArray(sanitized.markedExcerpts)) {
|
|
68
|
+
sanitized.markedExcerpts = sanitizeMarkedExcerpts(
|
|
69
|
+
sanitized.markedExcerpts,
|
|
70
|
+
`${location}.markedExcerpts`,
|
|
71
|
+
);
|
|
72
|
+
validateMarkedExcerpts(
|
|
73
|
+
sanitized.markedExcerpts,
|
|
74
|
+
sanitized.content,
|
|
75
|
+
`${location}.markedExcerpts`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
59
78
|
return sanitized;
|
|
60
79
|
}
|
|
61
80
|
|
|
81
|
+
function sanitizeMarkedExcerpts(excerpts, location) {
|
|
82
|
+
return excerpts.map((excerpt, index) => {
|
|
83
|
+
if (typeof excerpt !== "string") return excerpt;
|
|
84
|
+
const sanitized = sanitizeText(excerpt);
|
|
85
|
+
if (!sanitized) {
|
|
86
|
+
throw new CliError(
|
|
87
|
+
"validation_error",
|
|
88
|
+
`${location}[${index}] is empty after prohibited source labels are removed.`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
return sanitized;
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function validateMarkedExcerpts(excerpts, content, location) {
|
|
96
|
+
if (typeof content !== "string") return;
|
|
97
|
+
excerpts.forEach((excerpt, index) => {
|
|
98
|
+
if (typeof excerpt === "string" && !content.includes(excerpt)) {
|
|
99
|
+
throw new CliError(
|
|
100
|
+
"validation_error",
|
|
101
|
+
`${location}[${index}] must be an exact substring of the sanitized document content.`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
62
107
|
function prepareDocumentForWrite(document, location) {
|
|
63
108
|
const sanitized = sanitizeDocument(document, location);
|
|
64
109
|
if (
|
|
@@ -68,7 +113,7 @@ function prepareDocumentForWrite(document, location) {
|
|
|
68
113
|
) {
|
|
69
114
|
return sanitized;
|
|
70
115
|
}
|
|
71
|
-
const {
|
|
116
|
+
const { summary: _unsupportedSummary, ...writableDocument } = sanitized;
|
|
72
117
|
return writableDocument;
|
|
73
118
|
}
|
|
74
119
|
|
|
@@ -86,28 +131,92 @@ export function sanitizeDocumentBatchPayload(body) {
|
|
|
86
131
|
};
|
|
87
132
|
}
|
|
88
133
|
|
|
134
|
+
export function sanitizeDocumentReferencePayload(
|
|
135
|
+
body,
|
|
136
|
+
location = "documentReference",
|
|
137
|
+
) {
|
|
138
|
+
if (
|
|
139
|
+
body === null ||
|
|
140
|
+
Array.isArray(body) ||
|
|
141
|
+
typeof body !== "object"
|
|
142
|
+
) {
|
|
143
|
+
return body;
|
|
144
|
+
}
|
|
145
|
+
return prepareDocumentForWrite(body, location);
|
|
146
|
+
}
|
|
147
|
+
|
|
89
148
|
export function sanitizeKnowledgeTreePayload(body) {
|
|
90
|
-
|
|
149
|
+
const documents = Array.isArray(body.documents)
|
|
150
|
+
? body.documents.map((document, documentIndex) =>
|
|
151
|
+
prepareDocumentForWrite(
|
|
152
|
+
document,
|
|
153
|
+
`documents[${documentIndex}]`,
|
|
154
|
+
),
|
|
155
|
+
)
|
|
156
|
+
: body.documents;
|
|
157
|
+
const documentsByRef = new Map(
|
|
158
|
+
Array.isArray(documents)
|
|
159
|
+
? documents
|
|
160
|
+
.filter(
|
|
161
|
+
(document) =>
|
|
162
|
+
document &&
|
|
163
|
+
typeof document === "object" &&
|
|
164
|
+
typeof document.clientRef === "string",
|
|
165
|
+
)
|
|
166
|
+
.map((document) => [document.clientRef, document])
|
|
167
|
+
: [],
|
|
168
|
+
);
|
|
169
|
+
const nodes = Array.isArray(body.nodes)
|
|
170
|
+
? body.nodes.map((node, nodeIndex) => {
|
|
171
|
+
if (
|
|
172
|
+
node === null ||
|
|
173
|
+
Array.isArray(node) ||
|
|
174
|
+
typeof node !== "object"
|
|
175
|
+
) {
|
|
176
|
+
return node;
|
|
177
|
+
}
|
|
178
|
+
const sanitizedNode = { ...node };
|
|
179
|
+
if (Array.isArray(node.documentRefs)) {
|
|
180
|
+
sanitizedNode.documentRefs = node.documentRefs.map(
|
|
181
|
+
(reference, referenceIndex) => {
|
|
182
|
+
const location =
|
|
183
|
+
`nodes[${nodeIndex}].documentRefs[${referenceIndex}]`;
|
|
184
|
+
const sanitizedReference =
|
|
185
|
+
sanitizeDocumentReferencePayload(reference, location);
|
|
186
|
+
const document = documentsByRef.get(
|
|
187
|
+
sanitizedReference?.documentRef,
|
|
188
|
+
);
|
|
189
|
+
if (
|
|
190
|
+
document &&
|
|
191
|
+
Array.isArray(sanitizedReference.markedExcerpts)
|
|
192
|
+
) {
|
|
193
|
+
validateMarkedExcerpts(
|
|
194
|
+
sanitizedReference.markedExcerpts,
|
|
195
|
+
document.content,
|
|
196
|
+
`${location}.markedExcerpts`,
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
return sanitizedReference;
|
|
200
|
+
},
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
// Keep sanitizing the legacy shape so no document write path can bypass
|
|
204
|
+
// the permanent source-label policy.
|
|
205
|
+
if (Array.isArray(node.documents)) {
|
|
206
|
+
sanitizedNode.documents = node.documents.map(
|
|
207
|
+
(document, documentIndex) =>
|
|
208
|
+
prepareDocumentForWrite(
|
|
209
|
+
document,
|
|
210
|
+
`nodes[${nodeIndex}].documents[${documentIndex}]`,
|
|
211
|
+
),
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
return sanitizedNode;
|
|
215
|
+
})
|
|
216
|
+
: body.nodes;
|
|
91
217
|
return {
|
|
92
218
|
...body,
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
node === null ||
|
|
96
|
-
Array.isArray(node) ||
|
|
97
|
-
typeof node !== "object" ||
|
|
98
|
-
!Array.isArray(node.documents)
|
|
99
|
-
) {
|
|
100
|
-
return node;
|
|
101
|
-
}
|
|
102
|
-
return {
|
|
103
|
-
...node,
|
|
104
|
-
documents: node.documents.map((document, documentIndex) =>
|
|
105
|
-
prepareDocumentForWrite(
|
|
106
|
-
document,
|
|
107
|
-
`nodes[${nodeIndex}].documents[${documentIndex}]`,
|
|
108
|
-
),
|
|
109
|
-
),
|
|
110
|
-
};
|
|
111
|
-
}),
|
|
219
|
+
...(body.documents !== undefined ? { documents } : {}),
|
|
220
|
+
...(body.nodes !== undefined ? { nodes } : {}),
|
|
112
221
|
};
|
|
113
222
|
}
|