repzo 1.0.292 → 1.0.293

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/changelog.md +28 -0
  2. package/lib/index.d.ts +244 -2
  3. package/lib/index.js +541 -0
  4. package/lib/types/index.d.ts +4530 -1
  5. package/package.json +1 -1
  6. package/src/index.ts +1270 -0
  7. package/src/oas/activity-ai-object-detection-session-frame.yaml +600 -0
  8. package/src/oas/ai-object-detection-assigned-missions.yaml +283 -0
  9. package/src/oas/ai-object-detection-assignment-rule.yaml +357 -0
  10. package/src/oas/ai-object-detection-category.yaml +338 -0
  11. package/src/oas/ai-object-detection-dataset.yaml +342 -0
  12. package/src/oas/ai-object-detection-detection-settings.yaml +410 -0
  13. package/src/oas/ai-object-detection-inference.yaml +818 -0
  14. package/src/oas/ai-object-detection-label-group.yaml +265 -0
  15. package/src/oas/ai-object-detection-label-report.yaml +366 -0
  16. package/src/oas/ai-object-detection-label.yaml +395 -0
  17. package/src/oas/ai-object-detection-metric-result.yaml +734 -0
  18. package/src/oas/ai-object-detection-metric.yaml +559 -0
  19. package/src/oas/ai-object-detection-mission-results.yaml +370 -0
  20. package/src/oas/ai-object-detection-mission-set.yaml +250 -0
  21. package/src/oas/ai-object-detection-mission.yaml +349 -0
  22. package/src/oas/ai-object-detection-model-version-epoch.yaml +214 -0
  23. package/src/oas/ai-object-detection-model-version-train-agent.yaml +88 -0
  24. package/src/oas/ai-object-detection-model-version.yaml +567 -0
  25. package/src/oas/ai-object-detection-model.yaml +369 -0
  26. package/src/oas/ai-object-detection-segment.yaml +301 -0
  27. package/src/oas/ai-object-detection-session-analysis.yaml +1306 -0
  28. package/src/oas/ai-object-detection-session-election.yaml +193 -0
  29. package/src/oas/ai-object-detection-session-insight.yaml +418 -0
  30. package/src/oas/ai-object-detection-session.yaml +840 -0
  31. package/src/oas/ai-object-detection-settings.yaml +249 -0
  32. package/src/oas/ai-object-detection-task.yaml +1007 -0
  33. package/src/oas/object-detection-analytics-report.yaml +674 -0
  34. package/src/types/index.ts +5156 -1
@@ -0,0 +1,193 @@
1
+ openapi: 3.0.3
2
+ info:
3
+ title: Repzo API - AI Object Detection Session Election
4
+ version: 1.0.0
5
+ description: |
6
+ Materializes an election-playground result as a NEW object-detection
7
+ session. The dashboard's election playground simulates stack-then-elect
8
+ frame election over a session captured in the AR app's election-debug mode
9
+ (every stacked frame uploaded); this endpoint saves the elected subset as
10
+ a real session so it can go through the normal analysis pipeline.
11
+
12
+ **What it does.** The source session document is cloned — `device`,
13
+ `capture_settings`, `coverage_m2`, `client` and `status` copied,
14
+ `session_id` suffixed `-E<n>` (n = 1 + prior elections of that source)
15
+ and `source_session` set for provenance. The elected tasks are first
16
+ SCREENED against the namespace detection-settings ERROR tier in capture
17
+ order (`sharpness`, `tracking`, `too_near`/`too_far`, `yaw_delta`/
18
+ `pitch_delta`/`roll_delta` vs the previous kept frame); excluded frames
19
+ stay on the source session (they keep counting toward coverage) and are
20
+ listed under `election_excluded`. The surviving tasks are duplicated onto
21
+ the new session referencing the SAME media blobs (no image/depth copies)
22
+ with annotation state reset, so inference re-runs from scratch.
23
+
24
+ **Session gates & verdict.** `rejection_reasons` collects the session
25
+ gates that fired — `coverage_below_target` (device `coverage_m2` below the
26
+ configured target), `jump_detected` (adjacent kept frames farther apart
27
+ than a frame footprint, unless `allow_jump`), `too_many_elected` (kept
28
+ frames exceed coverage / avg footprint × allowance). `session_score` is
29
+ the average frame quality over ALL frames of the source session (0..1)
30
+ and `session_verdict` is `rejected` when any gate fired, else banded from
31
+ the score (`excellent` / `good` / `acceptable`). A `rejected` verdict
32
+ also suppresses the category auto-analysis on upload-complete.
33
+
34
+ **Who calls it & lifecycle.** Admin / API-key callers from the dashboard.
35
+ Create-only — `find`/`get`/`update`/`patch`/`remove` return 405. The new
36
+ row lives in the regular `ai-object-detection-session` collection
37
+ (namespace-scoped via the caller's token; `company_namespace` is
38
+ server-injected) and is read/managed through that service.
39
+ servers:
40
+ - url: https://sv.api.repzo.me
41
+ security:
42
+ - ApiKeyAuth: []
43
+ - JwtAuth: []
44
+ paths:
45
+ /ai-object-detection-session-election:
46
+ post:
47
+ summary: Save an elected frame subset as a new session
48
+ description: >-
49
+ Clones the source session and the chosen tasks (after error-tier
50
+ screening) into a new session with the same media references and a
51
+ fresh annotation state. Returns the new session document.
52
+ operationId: createAiObjectDetectionSessionElection
53
+ requestBody:
54
+ required: true
55
+ content:
56
+ application/json:
57
+ schema:
58
+ $ref: "#/components/schemas/ElectionRequest"
59
+ responses:
60
+ "201":
61
+ description: The newly created session document (a regular `ai-object-detection-session` row).
62
+ content:
63
+ application/json:
64
+ schema:
65
+ $ref: "#/components/schemas/ElectedSession"
66
+ "400":
67
+ description: >-
68
+ Missing `session`, empty `task_ids`, more than 500 task ids, task
69
+ ids that do not belong to the source session, or every elected
70
+ frame fails the error-tier limits (nothing to materialize).
71
+ "404":
72
+ description: Source session not found in the caller's namespace.
73
+ components:
74
+ securitySchemes:
75
+ ApiKeyAuth:
76
+ type: apiKey
77
+ in: header
78
+ name: api-key
79
+ description: |
80
+ Server-issued API key. Also accepted via the `x-api-key` header or the
81
+ `?apiKey=` query parameter as fallbacks.
82
+ JwtAuth:
83
+ type: apiKey
84
+ in: header
85
+ name: Authorization
86
+ description: |
87
+ Raw JWT in the `Authorization` header — **no `Bearer ` prefix**.
88
+ Obtained from `POST /authenticate` (admin / rep / client login).
89
+ schemas:
90
+ ElectionRequest:
91
+ type: object
92
+ required: [session, task_ids]
93
+ properties:
94
+ session:
95
+ type: string
96
+ description: Source `ai-object-detection-session` `_id`.
97
+ task_ids:
98
+ type: array
99
+ minItems: 1
100
+ maxItems: 500
101
+ items: { type: string }
102
+ description: >-
103
+ Elected task `_id`s — must all belong to the source session.
104
+ config_snapshot:
105
+ type: object
106
+ additionalProperties: true
107
+ description: >-
108
+ Optional playground formula parameters used for this election.
109
+ Accepted for provenance only — not persisted on the session.
110
+ company_namespace:
111
+ type: array
112
+ items: { type: string }
113
+ description: Optional tenant namespace override for SDK callers.
114
+ UserRef:
115
+ type: object
116
+ description: Compact actor reference (rep or admin).
117
+ properties:
118
+ _id: { type: string }
119
+ type: { type: string, enum: [admin, rep] }
120
+ name: { type: string }
121
+ rep: { type: string }
122
+ admin: { type: string }
123
+ ElectedSession:
124
+ type: object
125
+ description: >-
126
+ The materialized session — a regular `ai-object-detection-session`
127
+ document (see that service's `SessionSchema` for every field); the
128
+ election-specific fields are always present here.
129
+ properties:
130
+ _id: { type: string }
131
+ session_id:
132
+ type: string
133
+ description: "Source id with an election suffix, e.g. `AB12CD34-E1`."
134
+ source_session:
135
+ type: string
136
+ description: The source session `_id` (provenance).
137
+ status:
138
+ type: string
139
+ enum:
140
+ [open, uploaded, infer_in_progress, inferred, rearbitrating, failed]
141
+ description: Copied from the source session.
142
+ device: { type: object, additionalProperties: true }
143
+ capture_settings: { type: object, additionalProperties: true }
144
+ coverage_m2:
145
+ type: number
146
+ description: Copied from the source (total coverage INCLUDES the excluded frames).
147
+ client: { type: string }
148
+ session_score:
149
+ type: number
150
+ description: Average frame quality over ALL frames of the source session, 0..1.
151
+ session_verdict:
152
+ type: string
153
+ enum: [excellent, good, acceptable, rejected]
154
+ rejection_reasons:
155
+ type: array
156
+ items:
157
+ type: string
158
+ enum: [coverage_below_target, jump_detected, too_many_elected]
159
+ election_excluded:
160
+ type: array
161
+ description: Error-tier frames excluded from the clone.
162
+ items:
163
+ type: object
164
+ properties:
165
+ task: { type: string }
166
+ violations:
167
+ type: array
168
+ items:
169
+ type: string
170
+ enum:
171
+ [
172
+ sharpness,
173
+ tracking,
174
+ too_near,
175
+ too_far,
176
+ yaw_delta,
177
+ pitch_delta,
178
+ roll_delta,
179
+ ]
180
+ frames_total: { type: number }
181
+ frames_accepted: { type: number }
182
+ tasks_count: { type: number }
183
+ detections_count: { type: number }
184
+ objects_count: { type: number }
185
+ creator:
186
+ $ref: "#/components/schemas/UserRef"
187
+ disabled: { type: boolean }
188
+ company_namespace:
189
+ type: array
190
+ items: { type: string }
191
+ description: Tenant key. Server-injected — never accept from clients.
192
+ createdAt: { type: string, format: date-time }
193
+ updatedAt: { type: string, format: date-time }
@@ -0,0 +1,418 @@
1
+ openapi: 3.0.3
2
+ info:
3
+ title: Repzo API - AI Object Detection Session Insight
4
+ version: 1.0.0
5
+ description: |
6
+ **Session insight** — retail shelf intelligence computed ON READ from a
7
+ shelf-scan session's latest successful analysis (or an explicit analysis
8
+ id). Nothing is persisted or cached: edits to the analysis, the labels or
9
+ their product links reflect on the next request.
10
+
11
+ **What it derives.** Facings are the FRONT-ROW shelved objects of the
12
+ analysis's stored board-first shelf composition (composed on the fly for
13
+ legacy analyses that predate it), laid out along the scene's shelf axis û.
14
+ From them:
15
+ - `shelves[]` — facing counts, Σ linear cm, occupied span, utilization.
16
+ - `share_of_shelf` — rollup rows per vocabulary kind: `by_label` always;
17
+ `by_label_group` / `by_brand` / `by_category` / `by_subcategory` /
18
+ `by_product` appear only when the scene's labels LINK to entities of
19
+ that kind (links live on `ai-object-detection-label`).
20
+ - `blocking` — "merchandised together?": maximal same-shelf runs of a
21
+ group's facings, merged vertically across adjacent shelves when their
22
+ u-intervals overlap; interrupting labels are ranked.
23
+ - `objects` (`include_objects=true`) — planogram-style facing dump per
24
+ shelf, positions re-based to each shelf's left edge (cm).
25
+ - `adjacency` (`include_adjacency=true`) — who stands next to whom.
26
+
27
+ **Questions (POST).** Up to 20 free-text or structured questions answered
28
+ in order. Intent = share_of_shelf / blocking / adjacency; the target
29
+ resolves by id → exact name → substring → fuzzy tokens against the
30
+ scene's vocabulary; unresolved answers carry `reason` + `suggestions`.
31
+ Each answer includes a human-readable `narrative`.
32
+
33
+ **Config.** `default_facing_width_m` substitutes facings with no measured
34
+ size; `block_min_overlap_m` / `block_overlap_frac` tune the vertical
35
+ block merge. (`min_shelf_gap_m` / `shelf_gap_height_factor` are echoed
36
+ for contract compatibility — shelf levels now come from the analysis's
37
+ stored composition, tuned via the analysis `shelf_*` settings.)
38
+
39
+ Positions are metres (AR world frame); product sizes centimetres.
40
+ Namespace-scoped via the caller's token; read-only (PUT/PATCH/DELETE are
41
+ rejected). Admins read any session; a REP token reads only insights of
42
+ sessions THAT REP scanned (the mobile Retail Insights screen's read).
43
+ servers:
44
+ - url: https://sv.api.repzo.me
45
+ security:
46
+ - ApiKeyAuth: []
47
+ - JwtAuth: []
48
+ paths:
49
+ /ai-object-detection-session-insight:
50
+ get:
51
+ summary: Compute the insight for a session / analysis
52
+ operationId: findAiObjectDetectionSessionInsight
53
+ parameters:
54
+ - in: query
55
+ name: session
56
+ description: "Session `_id` — reads its LATEST successful analysis. One of `session` / `analysis` is required."
57
+ schema: { type: string }
58
+ - in: query
59
+ name: analysis
60
+ description: "Explicit analysis `_id` (must have `status: success`)."
61
+ schema: { type: string }
62
+ - in: query
63
+ name: include_objects
64
+ description: "Add the planogram-style facing dump per shelf (only the literal `true` is honoured)."
65
+ schema: { type: boolean }
66
+ - in: query
67
+ name: include_adjacency
68
+ description: "Add label-level neighbour counts (only the literal `true` is honoured)."
69
+ schema: { type: boolean }
70
+ responses:
71
+ "200":
72
+ description: The freshly computed insight payload (a single object — not paginated).
73
+ content:
74
+ application/json:
75
+ schema:
76
+ $ref: "#/components/schemas/SessionInsight"
77
+ "400":
78
+ description: Neither session nor analysis given, or the analysis is not successful.
79
+ "403":
80
+ description: A REP token asked for a session that rep did not scan.
81
+ "404":
82
+ description: No matching analysis (or the session has no successful analysis yet).
83
+ post:
84
+ summary: Compute the insight and answer questions
85
+ operationId: createAiObjectDetectionSessionInsight
86
+ requestBody:
87
+ required: true
88
+ content:
89
+ application/json:
90
+ schema:
91
+ type: object
92
+ properties:
93
+ session:
94
+ type: string
95
+ description: "Session `_id` — one of `session` / `analysis` is required."
96
+ analysis: { type: string }
97
+ questions:
98
+ type: array
99
+ maxItems: 20
100
+ description: Free-text strings and/or structured questions, answered in order.
101
+ items:
102
+ oneOf:
103
+ - type: string
104
+ - $ref: "#/components/schemas/InsightQuestion"
105
+ config:
106
+ $ref: "#/components/schemas/InsightConfig"
107
+ include_objects: { type: boolean }
108
+ include_adjacency: { type: boolean }
109
+ responses:
110
+ "201":
111
+ description: "The insight payload plus `answers[]` (one per question, in order)."
112
+ content:
113
+ application/json:
114
+ schema:
115
+ $ref: "#/components/schemas/SessionInsight"
116
+ "400":
117
+ description: "Neither session nor analysis given, more than 20 questions, or the analysis is not successful."
118
+ "403":
119
+ description: A REP token asked for a session that rep did not scan.
120
+ "404":
121
+ description: No matching analysis (or the session has no successful analysis yet).
122
+ /ai-object-detection-session-insight/{id}:
123
+ get:
124
+ summary: Compute the insight for a session (deep link)
125
+ description: The path id is read as a SESSION id.
126
+ operationId: getAiObjectDetectionSessionInsight
127
+ parameters:
128
+ - in: path
129
+ name: id
130
+ required: true
131
+ schema: { type: string }
132
+ responses:
133
+ "200":
134
+ description: The freshly computed insight payload.
135
+ content:
136
+ application/json:
137
+ schema:
138
+ $ref: "#/components/schemas/SessionInsight"
139
+ "403":
140
+ description: A REP token asked for a session that rep did not scan.
141
+ "404":
142
+ description: The session has no successful analysis yet.
143
+ components:
144
+ securitySchemes:
145
+ ApiKeyAuth:
146
+ type: apiKey
147
+ in: header
148
+ name: api-key
149
+ description: |
150
+ Server-issued API key. Also accepted via the `x-api-key` header or the
151
+ `?apiKey=` query parameter as fallbacks.
152
+ JwtAuth:
153
+ type: apiKey
154
+ in: header
155
+ name: Authorization
156
+ description: |
157
+ Raw JWT in the `Authorization` header — **no `Bearer ` prefix**.
158
+ Obtained from `POST /authenticate` (admin / rep / client login).
159
+ schemas:
160
+ InsightConfig:
161
+ type: object
162
+ description: All optional — invalid values fall back to the defaults.
163
+ properties:
164
+ min_shelf_gap_m:
165
+ type: number
166
+ default: 0.06
167
+ description: Echoed for compatibility — shelf levels come from the stored composition.
168
+ shelf_gap_height_factor:
169
+ type: number
170
+ default: 0.35
171
+ description: Echoed for compatibility.
172
+ default_facing_width_m:
173
+ type: number
174
+ default: 0.08
175
+ description: Fallback facing width (m) when a facing has no measured size.
176
+ block_min_overlap_m:
177
+ type: number
178
+ default: 0.03
179
+ description: Vertical block merge — required axis overlap (m) between adjacent-shelf runs…
180
+ block_overlap_frac:
181
+ type: number
182
+ default: 0.5
183
+ description: "…or this fraction of the narrower run's width, whichever is smaller."
184
+ InsightQuestion:
185
+ type: object
186
+ required: [type, target]
187
+ properties:
188
+ type:
189
+ type: string
190
+ enum: [share_of_shelf, blocking, adjacency]
191
+ target:
192
+ type: object
193
+ description: Resolved by `id` first, then exact name, then fuzzy match.
194
+ properties:
195
+ kind:
196
+ type: string
197
+ enum: [label, label_group, brand, category, subcategory, product]
198
+ id: { type: string }
199
+ name: { type: string }
200
+ InsightShareRow:
201
+ type: object
202
+ properties:
203
+ key: { type: string }
204
+ kind:
205
+ type: string
206
+ enum: [label, label_group, brand, category, subcategory, product]
207
+ name: { type: string }
208
+ facings: { type: number }
209
+ linear_cm: { type: number }
210
+ area_cm2: { type: number }
211
+ facing_share:
212
+ { type: number, description: 0..1 of all detected facings. }
213
+ linear_share: { type: number }
214
+ area_share: { type: number }
215
+ shelves:
216
+ type: array
217
+ items:
218
+ type: object
219
+ properties:
220
+ shelf_index: { type: number }
221
+ facings: { type: number }
222
+ linear_cm: { type: number }
223
+ linear_share_of_shelf: { type: number }
224
+ labels:
225
+ type: array
226
+ items:
227
+ type: object
228
+ properties:
229
+ label_id: { type: string }
230
+ label_name: { type: string }
231
+ facings: { type: number }
232
+ InsightBlocking:
233
+ type: object
234
+ properties:
235
+ key: { type: string }
236
+ name: { type: string }
237
+ narrative: { type: string }
238
+ present: { type: boolean }
239
+ facings: { type: number }
240
+ labels_present:
241
+ type: array
242
+ items:
243
+ type: object
244
+ properties:
245
+ label_id: { type: string }
246
+ label_name: { type: string }
247
+ facings: { type: number }
248
+ blocks_count: { type: number }
249
+ is_single_block:
250
+ type: boolean
251
+ description: True when every facing of the group sits in ONE contiguous block.
252
+ largest_block_facings: { type: number }
253
+ largest_block_share: { type: number }
254
+ blocks:
255
+ type: array
256
+ items:
257
+ type: object
258
+ properties:
259
+ shelves:
260
+ type: array
261
+ items: { type: number }
262
+ facings: { type: number }
263
+ runs:
264
+ type: array
265
+ items:
266
+ type: object
267
+ properties:
268
+ shelf_index: { type: number }
269
+ t0: { type: number, description: Left edge along û (m). }
270
+ t1: { type: number }
271
+ facings: { type: number }
272
+ facing_ids:
273
+ type: array
274
+ items: { type: string }
275
+ interrupters:
276
+ type: array
277
+ description: Foreign labels breaking the block, ranked by interrupting facings.
278
+ items:
279
+ type: object
280
+ properties:
281
+ label_id: { type: string }
282
+ label_name: { type: string }
283
+ facings: { type: number }
284
+ shelves_spanned:
285
+ type: array
286
+ items: { type: number }
287
+ SessionInsight:
288
+ type: object
289
+ properties:
290
+ session: { type: string }
291
+ analysis: { type: string }
292
+ model_version: { type: string }
293
+ analysis_finished_at: { type: number }
294
+ computed_at:
295
+ type: number
296
+ description: Unix ms — always freshly computed, never cached.
297
+ config_used:
298
+ $ref: "#/components/schemas/InsightConfig"
299
+ scene:
300
+ type: object
301
+ properties:
302
+ facings_placed: { type: number }
303
+ objects_skipped:
304
+ type: number
305
+ description: Analysis objects not counted as facings (back rows / unshelved).
306
+ shelf_count: { type: number }
307
+ axis:
308
+ type: object
309
+ description: Unit shelf-axis direction û in the horizontal plane.
310
+ properties:
311
+ dir_x: { type: number }
312
+ dir_z: { type: number }
313
+ totals:
314
+ type: object
315
+ properties:
316
+ facings: { type: number }
317
+ linear_cm: { type: number }
318
+ area_cm2: { type: number }
319
+ shelves:
320
+ type: array
321
+ items:
322
+ type: object
323
+ properties:
324
+ index: { type: number, description: 0 = bottom shelf. }
325
+ y_base_m:
326
+ { type: number, description: Board level (m, AR world frame). }
327
+ facings: { type: number }
328
+ linear_cm: { type: number }
329
+ span_cm: { type: number }
330
+ utilization:
331
+ type: number
332
+ description: linear_cm / span_cm — how much of the used span is product.
333
+ share_of_shelf:
334
+ type: object
335
+ description: "Keys: `by_label` always; `by_label_group` / `by_brand` / `by_category` / `by_subcategory` / `by_product` only when links exist."
336
+ additionalProperties:
337
+ type: array
338
+ items:
339
+ $ref: "#/components/schemas/InsightShareRow"
340
+ blocking:
341
+ type: object
342
+ description: Same keys as share_of_shelf; rows only for present groups.
343
+ additionalProperties:
344
+ type: array
345
+ items:
346
+ $ref: "#/components/schemas/InsightBlocking"
347
+ objects:
348
+ type: array
349
+ description: "Only with `include_objects=true` — facings left→right per shelf."
350
+ items:
351
+ type: object
352
+ properties:
353
+ shelf_index: { type: number }
354
+ facings:
355
+ type: array
356
+ items:
357
+ type: object
358
+ properties:
359
+ id: { type: string }
360
+ label_id: { type: string }
361
+ label_name: { type: string }
362
+ position: { type: number }
363
+ from_cm:
364
+ type: number
365
+ description: Re-based to the shelf's left-most facing.
366
+ to_cm: { type: number }
367
+ w_cm: { type: number }
368
+ h_cm: { type: number }
369
+ confidence: { type: number }
370
+ adjacency:
371
+ type: array
372
+ description: "Only with `include_adjacency=true`."
373
+ items:
374
+ type: object
375
+ properties:
376
+ label_id: { type: string }
377
+ label_name: { type: string }
378
+ neighbors:
379
+ type: array
380
+ items:
381
+ type: object
382
+ properties:
383
+ label_id: { type: string }
384
+ label_name: { type: string }
385
+ count: { type: number }
386
+ answers:
387
+ type: array
388
+ description: Present on POST responses only — one per question, in order.
389
+ items:
390
+ type: object
391
+ properties:
392
+ question:
393
+ oneOf:
394
+ - type: string
395
+ - $ref: "#/components/schemas/InsightQuestion"
396
+ type:
397
+ type: string
398
+ enum: [share_of_shelf, blocking, adjacency]
399
+ resolved: { type: boolean }
400
+ reason: { type: string }
401
+ suggestions:
402
+ type: array
403
+ items: { type: string }
404
+ target:
405
+ type: object
406
+ properties:
407
+ kind: { type: string }
408
+ name: { type: string }
409
+ label_ids:
410
+ type: array
411
+ items: { type: string }
412
+ result:
413
+ description: Share row, blocking result, or neighbour list per the question type.
414
+ oneOf:
415
+ - $ref: "#/components/schemas/InsightShareRow"
416
+ - $ref: "#/components/schemas/InsightBlocking"
417
+ - type: object
418
+ narrative: { type: string }