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 CHANGED
@@ -5,12 +5,19 @@
5
5
  ## 安装
6
6
 
7
7
  ```bash
8
- npm link
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
- 产品主数据不属于业务知识树层级;详情查询使用产品主数据 ID,并兼容历史知识树根节点 ID。固定维度为 L1;知识树回写是完整替换操作,必须一次提交全部 15-500 个 L2/L3/L4 动态节点。整树删除会保留产品根和固定维度,但级联删除动态节点及文档并解绑题目,只能在明确要求时执行。
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
- 所有文档写入都会在请求发出前从 `title`、`url`、`summary` 中移除“小荷”“小荷医疗”和“小荷 AI 医生”及其空格变体。当前写接口只接受 `title` 和 `summary`,输入中的 `url` 会在净化后从出站载荷移除,但 URL 净化能力作为永久兼容规则保留。该规则覆盖文档创建、批量 upsert、单文档更新和整树回写中的嵌套文档;标题清洗后为空会直接返回校验错误。
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,6 +1,6 @@
1
1
  {
2
2
  "name": "viatris-exam-cli",
3
- "version": "1.8.0",
3
+ "version": "1.9.0",
4
4
  "description": "Agent-friendly CLI for the Viatris exam OpenAPI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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.8.0
4
+ version: 1.9.0
5
5
  ---
6
6
 
7
7
  # Viatris Exam CLI
8
8
 
9
- Use the repository-local CLI for every supported API operation. Do not call the HTTP endpoints directly when the CLI covers the task.
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. Run commands from the repository root.
14
- 2. Prefer the saved credential. To initialize or rotate it, set `VIATRIS_API_KEY` and run `node src/cli.js login`; never place the key in command arguments, output, or logs.
15
- 3. Use production by default. Set `VIATRIS_API_ENV=development` only when the user requests development or testing.
16
- 4. Run `node src/cli.js describe --compact` when capability discovery or exact machine-readable metadata is needed.
17
- 5. Read [references/cli-reference.md](references/cli-reference.md) before constructing a write payload or handling a nonzero exit code.
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 CLI as `node src/cli.js ...`. Do not require `npm link`.
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
- - Document writes automatically remove 小荷 source-brand labels, including 小荷医疗 and whitespace variants of 小荷 AI 医生, from title, URL, and summary. Never reintroduce these labels after CLI sanitization.
34
- - Current document write APIs accept only `title` and `summary`; the CLI drops an input `url` before sending. Keep URL sanitization in the permanent sanitizer for backward and future compatibility.
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
- node src/cli.js login
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
- node src/cli.js describe --compact
46
- node src/cli.js --help
47
- node src/cli.js --version
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
- node src/cli.js products list --compact
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
- node src/cli.js products create --json '{
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
- node src/cli.js products get "$PRODUCT_ID" --compact
98
+ viatris-exam products get "$PRODUCT_ID" --compact
81
99
  ```
82
100
 
83
- Use the product master `id`; historical knowledge-tree root IDs remain accepted for compatibility. 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`.
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
- node src/cli.js products update "$PRODUCT_ID" --json '{
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
- node src/cli.js products update-knowledge-tree "$PRODUCT_ID" \
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
- "documents": [
161
+ "documentRefs": [
136
162
  {
137
- "title": "药品说明书",
138
- "summary": "与该知识点直接对应的证据摘录"
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 the payload. L2 nodes use `dimension:<code>` as `parentClientRef`; L3/L4 nodes use their direct parent's `clientRef`. Every L4 node needs at least one document. A successful PUT replaces the previous dynamic nodes, so do not send a partial tree.
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
- node src/cli.js products delete-knowledge-tree "$PRODUCT_ID"
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
- node src/cli.js knowledge tree lipitor --compact
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
- node src/cli.js knowledge nodes by-path --json \
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
- node src/cli.js knowledge nodes create --json \
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
- node src/cli.js knowledge nodes get "$NODE_ID"
186
- node src/cli.js knowledge nodes update "$NODE_ID" \
213
+ viatris-exam knowledge nodes get "$NODE_ID"
214
+ viatris-exam knowledge nodes update "$NODE_ID" \
187
215
  --json '{"name":"血脂异常与他汀治疗","sortOrder":5}'
188
- node src/cli.js knowledge nodes move "$NODE_ID" \
216
+ viatris-exam knowledge nodes move "$NODE_ID" \
189
217
  --json '{"newParentId":"NEW_PARENT_ID","sortOrder":30}'
190
- node src/cli.js knowledge nodes delete "$NODE_ID"
191
- node src/cli.js knowledge nodes delete "$NODE_ID" --recursive
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
- node src/cli.js knowledge documents list "$NODE_ID"
200
- node src/cli.js knowledge documents create "$NODE_ID" \
201
- --json '{"title":"说明书","summary":"摘录"}'
202
- node src/cli.js knowledge documents upsert "$NODE_ID" \
203
- --json '{"items":[{"title":"说明书","summary":"更新后的摘录"}]}'
204
- node src/cli.js knowledge documents update "$DOCUMENT_ID" \
205
- --json '{"summary":"更新后的摘要"}'
206
- node src/cli.js knowledge documents delete "$DOCUMENT_ID"
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 under the node. Batch upsert accepts 1-20 items and updates `summary` for existing titles. A single-document update changes only supplied fields; send `summary: null` to clear the summary. Current write contracts accept only `title` and `summary`; if input contains `url`, the CLI drops it before sending. Read responses may still contain historical URL data. Inspect a document before an explicitly requested delete.
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 `summary`. This applies to document create, batch upsert, single-document update, and documents nested in `products update-knowledge-tree`. URL filtering remains a permanent compatibility rule even though the current API drops URL from write payloads.
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
- node src/cli.js gen-tasks list \
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
- node src/cli.js gen-tasks get "$TASK_ID" --compact
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
- node src/cli.js gen-tasks submit "$TASK_ID" --input payload.json --compact
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
- node src/cli.js exam-records get "$RECORD_ID" --compact
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
- node src/cli.js exam-records scoring-rules "$RECORD_ID" --compact
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
- node src/cli.js scoring get "$TASK_ID" --compact
366
+ viatris-exam scoring get "$TASK_ID" --compact
333
367
  ```
334
368
 
335
369
  Submit scoring results:
336
370
 
337
371
  ```bash
338
- node src/cli.js scoring submit "$TASK_ID" --input results.json --compact
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
- node src/cli.js exams create --input exam.json --compact
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
- node src/cli.js questions create --input questions.json --compact
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
- node src/cli.js questions version "$QUESTION_ID" --input version.json --compact
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
- node src/cli.js medical-search search "阿托伐他汀 适应症" \
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
- node src/cli.js medical-search search --compact
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
- node src/cli.js questions create --input payload.json
420
- node src/cli.js questions create --input - < payload.json
421
- node src/cli.js questions create --json '{"items":[]}'
422
- printf '%s' '{"items":[]}' | node src/cli.js questions create
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
- export async function readJsonInput(options) {
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 (canReadStdin) {
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.8.0";
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 the product's dynamic L2/L3/L4 knowledge-tree nodes.",
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, summary?}",
126
- summary: "Idempotently create a document under a node.",
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?, summary?}",
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", "summary"],
6
- droppedWriteFields: ["url"],
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 { url: _unsupportedUrl, ...writableDocument } = sanitized;
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
- if (!Array.isArray(body.nodes)) return body;
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
- nodes: body.nodes.map((node, nodeIndex) => {
94
- if (
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
  }