@ketch.to/schema 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,12 @@
1
- # Temporary Holding Version
1
+ # @ketch.to/schema
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The card schema for [Ketch](https://ketch.to): JSON Schema (draft 2020-12), TypeScript types, validators and kind presets.
4
+
5
+ ```ts
6
+ import { validateNewCard, withKindDefaults, type NewCard } from "@ketch.to/schema";
7
+
8
+ const result = validateNewCard({ title: "Reply to Shaina's marketing plan", kind: "do" });
9
+ if (!result.ok) console.error(result.errors);
10
+ ```
11
+
12
+ The raw schema is exported as `@ketch.to/schema/card.schema.json`, and example cards for every kind as `@ketch.to/schema/fixtures`.
@@ -0,0 +1,601 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://ketch.to/schemas/card.json",
4
+ "title": "Ketch card schema",
5
+ "description": "The one data structure in Ketch. A card is a question an agent asks a person; it has exactly one outcome.",
6
+ "$defs": {
7
+ "Duration": {
8
+ "type": "string",
9
+ "description": "Relative duration: <integer><unit>, unit one of m (minutes), h (hours), d (days), w (weeks).",
10
+ "pattern": "^[1-9][0-9]{0,4}[mhdw]$",
11
+ "examples": [
12
+ "1h",
13
+ "3d",
14
+ "14d",
15
+ "2w"
16
+ ]
17
+ },
18
+ "CardId": {
19
+ "type": "string",
20
+ "pattern": "^card_[0-9A-HJKMNP-TV-Z]{26}$",
21
+ "description": "Server-assigned. 'card_' + ULID."
22
+ },
23
+ "ActionId": {
24
+ "type": "string",
25
+ "pattern": "^[a-z][a-z0-9_]{0,31}$",
26
+ "description": "Producer-chosen id, echoed back in the outcome. 'snooze' and 'not_good' are reserved and always available."
27
+ },
28
+ "ActionStyle": {
29
+ "type": "string",
30
+ "enum": [
31
+ "positive",
32
+ "snooze",
33
+ "neutral"
34
+ ],
35
+ "description": "positive: swipe-right target, accent colour. snooze: tap = 1h, hold = day slider. neutral: plain button."
36
+ },
37
+ "Action": {
38
+ "type": "object",
39
+ "additionalProperties": false,
40
+ "required": [
41
+ "id",
42
+ "label"
43
+ ],
44
+ "properties": {
45
+ "id": {
46
+ "$ref": "#/$defs/ActionId"
47
+ },
48
+ "label": {
49
+ "type": "string",
50
+ "minLength": 1,
51
+ "maxLength": 24
52
+ },
53
+ "style": {
54
+ "$ref": "#/$defs/ActionStyle",
55
+ "default": "neutral"
56
+ },
57
+ "input": {
58
+ "type": "string",
59
+ "enum": [
60
+ "optional",
61
+ "required"
62
+ ],
63
+ "description": "Open a text sheet before posting this action."
64
+ },
65
+ "snooze_for": {
66
+ "$ref": "#/$defs/Duration",
67
+ "description": "Only for style 'snooze': the default snooze on tap. Defaults to 1h."
68
+ }
69
+ }
70
+ },
71
+ "Actions": {
72
+ "type": "array",
73
+ "minItems": 1,
74
+ "maxItems": 4,
75
+ "items": {
76
+ "$ref": "#/$defs/Action"
77
+ }
78
+ },
79
+ "Source": {
80
+ "type": "object",
81
+ "additionalProperties": false,
82
+ "properties": {
83
+ "app": {
84
+ "type": "string",
85
+ "minLength": 1,
86
+ "maxLength": 40,
87
+ "description": "Defaults to the API key's name."
88
+ },
89
+ "context": {
90
+ "type": "string",
91
+ "minLength": 1,
92
+ "maxLength": 120
93
+ }
94
+ }
95
+ },
96
+ "Input": {
97
+ "type": "object",
98
+ "additionalProperties": false,
99
+ "properties": {
100
+ "placeholder": {
101
+ "type": "string",
102
+ "maxLength": 80
103
+ },
104
+ "spawns": {
105
+ "type": "boolean",
106
+ "default": false,
107
+ "description": "Each entry the person types becomes a new card (event card.spawned)."
108
+ }
109
+ }
110
+ },
111
+ "Recurrence": {
112
+ "type": "object",
113
+ "additionalProperties": false,
114
+ "required": [
115
+ "every"
116
+ ],
117
+ "properties": {
118
+ "every": {
119
+ "$ref": "#/$defs/Duration"
120
+ }
121
+ }
122
+ },
123
+ "Kind": {
124
+ "type": "string",
125
+ "enum": [
126
+ "do",
127
+ "choose",
128
+ "checkin",
129
+ "decide",
130
+ "read",
131
+ "recap"
132
+ ],
133
+ "description": "Preset. Fills in default actions (and recurrence/input for some) when the producer omits them. Never requires app code."
134
+ },
135
+ "Priority": {
136
+ "type": "integer",
137
+ "minimum": 1,
138
+ "maximum": 5,
139
+ "default": 3,
140
+ "description": "1 urgent … 5 ambient. ≤2 pushes a notification."
141
+ },
142
+ "Link": {
143
+ "type": "string",
144
+ "pattern": "^(https://|shortcuts://)[^\\s]+$",
145
+ "maxLength": 2048,
146
+ "description": "Opened with the system. https:// or shortcuts:// only."
147
+ },
148
+ "Meta": {
149
+ "type": "object",
150
+ "description": "Producer's own bag. Echoed back untouched in webhooks and outcomes."
151
+ },
152
+ "CardFields": {
153
+ "type": "object",
154
+ "properties": {
155
+ "title": {
156
+ "type": "string",
157
+ "minLength": 1,
158
+ "maxLength": 140
159
+ },
160
+ "body": {
161
+ "type": "string",
162
+ "maxLength": 4000,
163
+ "description": "Plain text or light markdown."
164
+ },
165
+ "source": {
166
+ "$ref": "#/$defs/Source"
167
+ },
168
+ "link": {
169
+ "$ref": "#/$defs/Link"
170
+ },
171
+ "priority": {
172
+ "$ref": "#/$defs/Priority"
173
+ },
174
+ "kind": {
175
+ "$ref": "#/$defs/Kind",
176
+ "default": "do"
177
+ },
178
+ "actions": {
179
+ "$ref": "#/$defs/Actions"
180
+ },
181
+ "input": {
182
+ "$ref": "#/$defs/Input"
183
+ },
184
+ "recurrence": {
185
+ "$ref": "#/$defs/Recurrence"
186
+ },
187
+ "show_after": {
188
+ "type": [
189
+ "string",
190
+ "null"
191
+ ],
192
+ "format": "date-time",
193
+ "description": "Hidden until then. Snooze writes this."
194
+ },
195
+ "expires_at": {
196
+ "type": [
197
+ "string",
198
+ "null"
199
+ ],
200
+ "format": "date-time",
201
+ "description": "Retired automatically with outcome action 'expired'."
202
+ },
203
+ "meta": {
204
+ "$ref": "#/$defs/Meta"
205
+ }
206
+ }
207
+ },
208
+ "NewCard": {
209
+ "description": "What a producer POSTs. Everything but title is optional; kind fills in actions.",
210
+ "allOf": [
211
+ {
212
+ "$ref": "#/$defs/CardFields"
213
+ }
214
+ ],
215
+ "type": "object",
216
+ "additionalProperties": false,
217
+ "required": [
218
+ "title"
219
+ ],
220
+ "properties": {
221
+ "title": true,
222
+ "body": true,
223
+ "source": true,
224
+ "link": true,
225
+ "priority": true,
226
+ "kind": true,
227
+ "actions": true,
228
+ "input": true,
229
+ "recurrence": true,
230
+ "show_after": true,
231
+ "expires_at": true,
232
+ "meta": true
233
+ }
234
+ },
235
+ "CardPatch": {
236
+ "description": "What PATCH /cards/:id accepts. Producer-side edits only.",
237
+ "type": "object",
238
+ "additionalProperties": false,
239
+ "minProperties": 1,
240
+ "properties": {
241
+ "title": {
242
+ "type": "string",
243
+ "minLength": 1,
244
+ "maxLength": 140
245
+ },
246
+ "body": {
247
+ "type": [
248
+ "string",
249
+ "null"
250
+ ],
251
+ "maxLength": 4000
252
+ },
253
+ "actions": {
254
+ "$ref": "#/$defs/Actions"
255
+ },
256
+ "show_after": {
257
+ "type": [
258
+ "string",
259
+ "null"
260
+ ],
261
+ "format": "date-time"
262
+ },
263
+ "priority": {
264
+ "$ref": "#/$defs/Priority"
265
+ },
266
+ "link": {
267
+ "anyOf": [
268
+ {
269
+ "$ref": "#/$defs/Link"
270
+ },
271
+ {
272
+ "type": "null"
273
+ }
274
+ ]
275
+ },
276
+ "expires_at": {
277
+ "type": [
278
+ "string",
279
+ "null"
280
+ ],
281
+ "format": "date-time"
282
+ },
283
+ "meta": {
284
+ "$ref": "#/$defs/Meta"
285
+ }
286
+ }
287
+ },
288
+ "Status": {
289
+ "type": "string",
290
+ "enum": [
291
+ "open",
292
+ "snoozed",
293
+ "done"
294
+ ],
295
+ "description": "open: in the queue. snoozed: open but show_after is in the future. done: retired (acted, deleted or expired)."
296
+ },
297
+ "Outcome": {
298
+ "type": "object",
299
+ "additionalProperties": false,
300
+ "required": [
301
+ "action",
302
+ "text",
303
+ "at",
304
+ "snoozed_until"
305
+ ],
306
+ "properties": {
307
+ "action": {
308
+ "$ref": "#/$defs/ActionId"
309
+ },
310
+ "text": {
311
+ "type": [
312
+ "string",
313
+ "null"
314
+ ],
315
+ "maxLength": 4000
316
+ },
317
+ "at": {
318
+ "type": "string",
319
+ "format": "date-time"
320
+ },
321
+ "snoozed_until": {
322
+ "type": [
323
+ "string",
324
+ "null"
325
+ ],
326
+ "format": "date-time"
327
+ }
328
+ }
329
+ },
330
+ "OutcomeInput": {
331
+ "description": "What the app POSTs to /cards/:id/outcome. One per card (snoozes excepted).",
332
+ "type": "object",
333
+ "additionalProperties": false,
334
+ "required": [
335
+ "action"
336
+ ],
337
+ "properties": {
338
+ "action": {
339
+ "$ref": "#/$defs/ActionId"
340
+ },
341
+ "text": {
342
+ "type": [
343
+ "string",
344
+ "null"
345
+ ],
346
+ "maxLength": 4000
347
+ },
348
+ "snoozed_until": {
349
+ "type": [
350
+ "string",
351
+ "null"
352
+ ],
353
+ "format": "date-time",
354
+ "description": "Required when action is 'snooze' (or any snooze-style action)."
355
+ },
356
+ "spawn": {
357
+ "type": "array",
358
+ "maxItems": 20,
359
+ "items": {
360
+ "type": "string",
361
+ "minLength": 1,
362
+ "maxLength": 140
363
+ },
364
+ "description": "Titles of new cards to create from the card's spawning input."
365
+ }
366
+ }
367
+ },
368
+ "Card": {
369
+ "description": "A card as the server returns it.",
370
+ "allOf": [
371
+ {
372
+ "$ref": "#/$defs/CardFields"
373
+ }
374
+ ],
375
+ "type": "object",
376
+ "additionalProperties": false,
377
+ "required": [
378
+ "id",
379
+ "title",
380
+ "source",
381
+ "priority",
382
+ "kind",
383
+ "actions",
384
+ "status",
385
+ "created_at",
386
+ "updated_at"
387
+ ],
388
+ "properties": {
389
+ "id": {
390
+ "$ref": "#/$defs/CardId"
391
+ },
392
+ "title": true,
393
+ "body": true,
394
+ "link": true,
395
+ "priority": true,
396
+ "kind": true,
397
+ "actions": true,
398
+ "input": true,
399
+ "recurrence": true,
400
+ "show_after": true,
401
+ "expires_at": true,
402
+ "meta": true,
403
+ "source": {
404
+ "type": "object",
405
+ "additionalProperties": false,
406
+ "required": [
407
+ "app"
408
+ ],
409
+ "properties": {
410
+ "app": {
411
+ "type": "string",
412
+ "minLength": 1,
413
+ "maxLength": 40
414
+ },
415
+ "context": {
416
+ "type": "string",
417
+ "minLength": 1,
418
+ "maxLength": 120
419
+ }
420
+ }
421
+ },
422
+ "status": {
423
+ "$ref": "#/$defs/Status"
424
+ },
425
+ "parent_id": {
426
+ "anyOf": [
427
+ {
428
+ "$ref": "#/$defs/CardId"
429
+ },
430
+ {
431
+ "type": "null"
432
+ }
433
+ ],
434
+ "description": "Set on cards spawned from another card's input."
435
+ },
436
+ "outcome": {
437
+ "anyOf": [
438
+ {
439
+ "$ref": "#/$defs/Outcome"
440
+ },
441
+ {
442
+ "type": "null"
443
+ }
444
+ ],
445
+ "description": "The last outcome, if any. Recurring cards keep their latest."
446
+ },
447
+ "created_at": {
448
+ "type": "string",
449
+ "format": "date-time"
450
+ },
451
+ "updated_at": {
452
+ "type": "string",
453
+ "format": "date-time"
454
+ },
455
+ "retired_at": {
456
+ "type": [
457
+ "string",
458
+ "null"
459
+ ],
460
+ "format": "date-time"
461
+ }
462
+ }
463
+ },
464
+ "WebhookEvent": {
465
+ "type": "string",
466
+ "enum": [
467
+ "card.acted",
468
+ "card.snoozed",
469
+ "card.spawned"
470
+ ]
471
+ },
472
+ "NewWebhook": {
473
+ "type": "object",
474
+ "additionalProperties": false,
475
+ "required": [
476
+ "url",
477
+ "secret",
478
+ "events"
479
+ ],
480
+ "properties": {
481
+ "url": {
482
+ "type": "string",
483
+ "pattern": "^https?://[^\\s]+$",
484
+ "maxLength": 2048
485
+ },
486
+ "secret": {
487
+ "type": "string",
488
+ "minLength": 16,
489
+ "maxLength": 256
490
+ },
491
+ "events": {
492
+ "type": "array",
493
+ "minItems": 1,
494
+ "uniqueItems": true,
495
+ "items": {
496
+ "$ref": "#/$defs/WebhookEvent"
497
+ }
498
+ }
499
+ }
500
+ },
501
+ "Webhook": {
502
+ "type": "object",
503
+ "additionalProperties": false,
504
+ "required": [
505
+ "id",
506
+ "url",
507
+ "events",
508
+ "created_at"
509
+ ],
510
+ "properties": {
511
+ "id": {
512
+ "type": "string",
513
+ "pattern": "^wh_[0-9A-HJKMNP-TV-Z]{26}$"
514
+ },
515
+ "url": {
516
+ "type": "string"
517
+ },
518
+ "events": {
519
+ "type": "array",
520
+ "items": {
521
+ "$ref": "#/$defs/WebhookEvent"
522
+ }
523
+ },
524
+ "created_at": {
525
+ "type": "string",
526
+ "format": "date-time"
527
+ }
528
+ }
529
+ },
530
+ "WebhookPayload": {
531
+ "description": "Body of every webhook POST. HMAC-SHA256 hex of the raw body, keyed with the webhook secret, in X-Ketch-Signature.",
532
+ "type": "object",
533
+ "additionalProperties": false,
534
+ "required": [
535
+ "id",
536
+ "event",
537
+ "at"
538
+ ],
539
+ "properties": {
540
+ "id": {
541
+ "type": "string",
542
+ "description": "Delivery id. Same id on every retry; use it to dedupe."
543
+ },
544
+ "event": {
545
+ "$ref": "#/$defs/WebhookEvent"
546
+ },
547
+ "at": {
548
+ "type": "string",
549
+ "format": "date-time"
550
+ },
551
+ "card": {
552
+ "$ref": "#/$defs/Card"
553
+ },
554
+ "outcome": {
555
+ "$ref": "#/$defs/Outcome"
556
+ },
557
+ "parent": {
558
+ "$ref": "#/$defs/Card"
559
+ }
560
+ },
561
+ "oneOf": [
562
+ {
563
+ "properties": {
564
+ "event": {
565
+ "const": "card.acted"
566
+ },
567
+ "parent": false
568
+ },
569
+ "required": [
570
+ "card",
571
+ "outcome"
572
+ ]
573
+ },
574
+ {
575
+ "properties": {
576
+ "event": {
577
+ "const": "card.snoozed"
578
+ },
579
+ "parent": false
580
+ },
581
+ "required": [
582
+ "card",
583
+ "outcome"
584
+ ]
585
+ },
586
+ {
587
+ "properties": {
588
+ "event": {
589
+ "const": "card.spawned"
590
+ },
591
+ "outcome": false
592
+ },
593
+ "required": [
594
+ "parent",
595
+ "card"
596
+ ]
597
+ }
598
+ ]
599
+ }
600
+ }
601
+ }