@pepys/kit 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.
Files changed (68) hide show
  1. package/README.md +63 -2
  2. package/package.json +34 -4
  3. package/src/constants.ts +1 -0
  4. package/src/consumers/define-consumer.ts +20 -0
  5. package/src/consumers/types.ts +48 -0
  6. package/src/delivery-checks/build-delivery-proof.ts +12 -0
  7. package/src/delivery-checks/is-delivery-authentic.ts +49 -0
  8. package/src/deployments/check-deployment.ts +445 -0
  9. package/src/deployments/define-deployment.ts +7 -0
  10. package/src/deployments/define-plugin.ts +56 -0
  11. package/src/deployments/list-receive-routes.ts +27 -0
  12. package/src/deployments/types.ts +112 -0
  13. package/src/entity-ids/build-entity-id.ts +16 -0
  14. package/src/entity-ids/entity-id-failure.ts +6 -0
  15. package/src/entity-ids/entity-id-parts.ts +5 -0
  16. package/src/entity-ids/fact-kinds.ts +3 -0
  17. package/src/entity-ids/find-entity-id-failure.ts +25 -0
  18. package/src/entity-ids/parse-entity-id.ts +27 -0
  19. package/src/fact-envelope-cases.json +464 -0
  20. package/src/facts/build-enrichment-fact-id.ts +15 -0
  21. package/src/facts/build-event-fact-id.ts +11 -0
  22. package/src/facts/build-fact-hash.ts +6 -0
  23. package/src/facts/build-manual-fact-id.ts +7 -0
  24. package/src/facts/build-observation-fact-id.ts +15 -0
  25. package/src/facts/build-webhook-fact-id.ts +11 -0
  26. package/src/facts/parse-fact-envelope.ts +44 -0
  27. package/src/facts/parse-stream-name.ts +33 -0
  28. package/src/generators/build-payload-shape.ts +80 -0
  29. package/src/generators/collect-view-kinds.ts +8 -0
  30. package/src/generators/format-column-type.ts +16 -0
  31. package/src/generators/format-entity-ids.ts +46 -0
  32. package/src/generators/format-fact-reader.ts +194 -0
  33. package/src/generators/format-literal.ts +13 -0
  34. package/src/generators/format-model-contract.ts +23 -0
  35. package/src/generators/format-operations.ts +109 -0
  36. package/src/generators/format-payload-shape.ts +181 -0
  37. package/src/generators/format-property-access.ts +6 -0
  38. package/src/generators/format-property-name.ts +6 -0
  39. package/src/generators/format-row-readers.ts +116 -0
  40. package/src/generators/generate-boundary-glue.ts +60 -0
  41. package/src/generators/generate-sqlite-schema.ts +78 -0
  42. package/src/generators/is-identifier.ts +5 -0
  43. package/src/generators/quote-sql-identifier.ts +3 -0
  44. package/src/generators/to-entity-id-type-name.ts +6 -0
  45. package/src/generators/to-pascal-case.ts +7 -0
  46. package/src/generators.ts +4 -0
  47. package/src/ids/types.ts +25 -0
  48. package/src/index.ts +37 -0
  49. package/src/link-types/define-link-type.ts +9 -0
  50. package/src/link-types/types.ts +15 -0
  51. package/src/links/build-link-registry.ts +7 -0
  52. package/src/links/find-link-problem.ts +53 -0
  53. package/src/links/link-check-cases.json +274 -0
  54. package/src/links/types.ts +27 -0
  55. package/src/queries/define-query.ts +14 -0
  56. package/src/queries/types.ts +61 -0
  57. package/src/send/create-fact-batch-sender.ts +143 -0
  58. package/src/send/types.ts +25 -0
  59. package/src/send.ts +2 -0
  60. package/src/sources/define-fact-family.ts +10 -0
  61. package/src/sources/define-source.ts +7 -0
  62. package/src/sources/read-fact-batch.ts +145 -0
  63. package/src/sources/receive-fact-batch.ts +11 -0
  64. package/src/sources/types.ts +170 -0
  65. package/src/store-reader/store-reader.ts +76 -0
  66. package/src/tables/define-table.ts +13 -0
  67. package/src/tables/table-row.ts +20 -0
  68. package/src/types.ts +47 -0
@@ -0,0 +1,464 @@
1
+ {
2
+ "cases": [
3
+ {
4
+ "title": "accepts a webhook fact ID",
5
+ "envelope": {
6
+ "id": "webhook:github:d-1",
7
+ "type": "github.pull_request.opened",
8
+ "v": 1,
9
+ "api": "github/2022-11-28",
10
+ "entity": "github:pull_request:77",
11
+ "at": "2026-10-02T10:30:00Z",
12
+ "payload": {}
13
+ },
14
+ "isValid": true
15
+ },
16
+ {
17
+ "title": "accepts an observation fact ID",
18
+ "envelope": {
19
+ "id": "observation:0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a",
20
+ "type": "github.pull_request.opened",
21
+ "v": 1,
22
+ "api": "github/2022-11-28",
23
+ "entity": "github:pull_request:77",
24
+ "at": "2026-10-02T10:30:00Z",
25
+ "payload": {}
26
+ },
27
+ "isValid": true
28
+ },
29
+ {
30
+ "title": "accepts an enrichment fact ID",
31
+ "envelope": {
32
+ "id": "enrichment:b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1",
33
+ "type": "github.pull_request.opened",
34
+ "v": 1,
35
+ "api": "github/2022-11-28",
36
+ "entity": "github:pull_request:77",
37
+ "at": "2026-10-02T10:30:00Z",
38
+ "payload": {}
39
+ },
40
+ "isValid": true
41
+ },
42
+ {
43
+ "title": "accepts a manual fact ID",
44
+ "envelope": {
45
+ "id": "manual:c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2",
46
+ "type": "github.pull_request.opened",
47
+ "v": 1,
48
+ "api": "github/2022-11-28",
49
+ "entity": "github:pull_request:77",
50
+ "at": "2026-10-02T10:30:00Z",
51
+ "payload": {}
52
+ },
53
+ "isValid": true
54
+ },
55
+ {
56
+ "title": "accepts an event fact ID",
57
+ "envelope": {
58
+ "id": "event:boxxy:evt-0001",
59
+ "type": "boxxy.attempt.started",
60
+ "v": 1,
61
+ "api": "boxxy/1",
62
+ "entity": "boxxy:item:0193a1b2-0000-7000-8000-000000000001",
63
+ "at": "2026-10-02T10:30:00Z",
64
+ "payload": {}
65
+ },
66
+ "isValid": true
67
+ },
68
+ {
69
+ "title": "accepts an event fact ID whose event ID holds a colon and a dot",
70
+ "envelope": {
71
+ "id": "event:telemetry:claude_code.api_request:req_01",
72
+ "type": "telemetry.claude_code.api_request",
73
+ "v": 1,
74
+ "api": "otlp-logs/1",
75
+ "entity": "telemetry:session:s-9",
76
+ "at": "2026-10-02T10:30:00Z",
77
+ "payload": {}
78
+ },
79
+ "isValid": true
80
+ },
81
+ {
82
+ "title": "accepts an event fact ID whose event ID is a hash key",
83
+ "envelope": {
84
+ "id": "event:telemetry:codex.sse_event:h-5e1c9a07",
85
+ "type": "telemetry.claude_code.api_request",
86
+ "v": 1,
87
+ "api": "otlp-logs/1",
88
+ "entity": "telemetry:session:s-9",
89
+ "at": "2026-10-02T10:30:00Z",
90
+ "payload": {}
91
+ },
92
+ "isValid": true
93
+ },
94
+ {
95
+ "title": "accepts an actor and a cursor",
96
+ "envelope": {
97
+ "id": "webhook:github:d-1",
98
+ "type": "github.pull_request.opened",
99
+ "v": 1,
100
+ "api": "github/2022-11-28",
101
+ "entity": "github:pull_request:77",
102
+ "at": "2026-10-02T10:30:00Z",
103
+ "payload": {},
104
+ "actor": "github:user:583231",
105
+ "cursor": "page-7"
106
+ },
107
+ "isValid": true
108
+ },
109
+ {
110
+ "title": "accepts fractional seconds",
111
+ "envelope": {
112
+ "id": "webhook:github:d-1",
113
+ "type": "github.pull_request.opened",
114
+ "v": 1,
115
+ "api": "github/2022-11-28",
116
+ "entity": "github:pull_request:77",
117
+ "at": "2026-10-02T10:30:00.123456Z",
118
+ "payload": {}
119
+ },
120
+ "isValid": true
121
+ },
122
+ {
123
+ "title": "accepts a leap day",
124
+ "envelope": {
125
+ "id": "webhook:github:d-1",
126
+ "type": "github.pull_request.opened",
127
+ "v": 1,
128
+ "api": "github/2022-11-28",
129
+ "entity": "github:pull_request:77",
130
+ "at": "2028-02-29T10:30:00Z",
131
+ "payload": {}
132
+ },
133
+ "isValid": true
134
+ },
135
+ {
136
+ "title": "refuses an ID of no fact kind",
137
+ "envelope": {
138
+ "id": "not-a-fact-id",
139
+ "type": "github.pull_request.opened",
140
+ "v": 1,
141
+ "api": "github/2022-11-28",
142
+ "entity": "github:pull_request:77",
143
+ "at": "2026-10-02T10:30:00Z",
144
+ "payload": {}
145
+ },
146
+ "isValid": false
147
+ },
148
+ {
149
+ "title": "refuses a manual fact ID with a short hash",
150
+ "envelope": {
151
+ "id": "manual:abc",
152
+ "type": "github.pull_request.opened",
153
+ "v": 1,
154
+ "api": "github/2022-11-28",
155
+ "entity": "github:pull_request:77",
156
+ "at": "2026-10-02T10:30:00Z",
157
+ "payload": {}
158
+ },
159
+ "isValid": false
160
+ },
161
+ {
162
+ "title": "refuses a webhook fact ID with an upper-case source",
163
+ "envelope": {
164
+ "id": "webhook:GitHub:d-1",
165
+ "type": "github.pull_request.opened",
166
+ "v": 1,
167
+ "api": "github/2022-11-28",
168
+ "entity": "github:pull_request:77",
169
+ "at": "2026-10-02T10:30:00Z",
170
+ "payload": {}
171
+ },
172
+ "isValid": false
173
+ },
174
+ {
175
+ "title": "refuses a webhook fact ID with a line feed",
176
+ "envelope": {
177
+ "id": "webhook:github:d\n1",
178
+ "type": "github.pull_request.opened",
179
+ "v": 1,
180
+ "api": "github/2022-11-28",
181
+ "entity": "github:pull_request:77",
182
+ "at": "2026-10-02T10:30:00Z",
183
+ "payload": {}
184
+ },
185
+ "isValid": false
186
+ },
187
+ {
188
+ "title": "refuses a webhook fact ID with a carriage return",
189
+ "envelope": {
190
+ "id": "webhook:github:d\r1",
191
+ "type": "github.pull_request.opened",
192
+ "v": 1,
193
+ "api": "github/2022-11-28",
194
+ "entity": "github:pull_request:77",
195
+ "at": "2026-10-02T10:30:00Z",
196
+ "payload": {}
197
+ },
198
+ "isValid": false
199
+ },
200
+ {
201
+ "title": "refuses a webhook fact ID with a line separator",
202
+ "envelope": {
203
+ "id": "webhook:github:d\u20281",
204
+ "type": "github.pull_request.opened",
205
+ "v": 1,
206
+ "api": "github/2022-11-28",
207
+ "entity": "github:pull_request:77",
208
+ "at": "2026-10-02T10:30:00Z",
209
+ "payload": {}
210
+ },
211
+ "isValid": false
212
+ },
213
+ {
214
+ "title": "refuses a webhook fact ID with a paragraph separator",
215
+ "envelope": {
216
+ "id": "webhook:github:d\u20291",
217
+ "type": "github.pull_request.opened",
218
+ "v": 1,
219
+ "api": "github/2022-11-28",
220
+ "entity": "github:pull_request:77",
221
+ "at": "2026-10-02T10:30:00Z",
222
+ "payload": {}
223
+ },
224
+ "isValid": false
225
+ },
226
+ {
227
+ "title": "refuses an event fact ID with no event ID",
228
+ "envelope": {
229
+ "id": "event:boxxy:",
230
+ "type": "boxxy.attempt.started",
231
+ "v": 1,
232
+ "api": "boxxy/1",
233
+ "entity": "boxxy:item:0193a1b2-0000-7000-8000-000000000001",
234
+ "at": "2026-10-02T10:30:00Z",
235
+ "payload": {}
236
+ },
237
+ "isValid": false
238
+ },
239
+ {
240
+ "title": "refuses an event fact ID with an upper-case source",
241
+ "envelope": {
242
+ "id": "event:Boxxy:evt-0001",
243
+ "type": "boxxy.attempt.started",
244
+ "v": 1,
245
+ "api": "boxxy/1",
246
+ "entity": "boxxy:item:0193a1b2-0000-7000-8000-000000000001",
247
+ "at": "2026-10-02T10:30:00Z",
248
+ "payload": {}
249
+ },
250
+ "isValid": false
251
+ },
252
+ {
253
+ "title": "refuses an empty type",
254
+ "envelope": {
255
+ "id": "webhook:github:d-1",
256
+ "type": "",
257
+ "v": 1,
258
+ "api": "github/2022-11-28",
259
+ "entity": "github:pull_request:77",
260
+ "at": "2026-10-02T10:30:00Z",
261
+ "payload": {}
262
+ },
263
+ "isValid": false
264
+ },
265
+ {
266
+ "title": "refuses an envelope version other than 1",
267
+ "envelope": {
268
+ "id": "webhook:github:d-1",
269
+ "type": "github.pull_request.opened",
270
+ "v": 2,
271
+ "api": "github/2022-11-28",
272
+ "entity": "github:pull_request:77",
273
+ "at": "2026-10-02T10:30:00Z",
274
+ "payload": {}
275
+ },
276
+ "isValid": false
277
+ },
278
+ {
279
+ "title": "refuses an api that is not text",
280
+ "envelope": {
281
+ "id": "webhook:github:d-1",
282
+ "type": "github.pull_request.opened",
283
+ "v": 1,
284
+ "api": 7,
285
+ "entity": "github:pull_request:77",
286
+ "at": "2026-10-02T10:30:00Z",
287
+ "payload": {}
288
+ },
289
+ "isValid": false
290
+ },
291
+ {
292
+ "title": "refuses an entity ID with no kind",
293
+ "envelope": {
294
+ "id": "webhook:github:d-1",
295
+ "type": "github.pull_request.opened",
296
+ "v": 1,
297
+ "api": "github/2022-11-28",
298
+ "entity": "github:pull_request",
299
+ "at": "2026-10-02T10:30:00Z",
300
+ "payload": {}
301
+ },
302
+ "isValid": false
303
+ },
304
+ {
305
+ "title": "refuses an entity ID whose source is a fact kind",
306
+ "envelope": {
307
+ "id": "webhook:github:d-1",
308
+ "type": "github.pull_request.opened",
309
+ "v": 1,
310
+ "api": "github/2022-11-28",
311
+ "entity": "manual:pr:7",
312
+ "at": "2026-10-02T10:30:00Z",
313
+ "payload": {}
314
+ },
315
+ "isValid": false
316
+ },
317
+ {
318
+ "title": "refuses an entity ID whose source is the event fact kind",
319
+ "envelope": {
320
+ "id": "webhook:github:d-1",
321
+ "type": "github.pull_request.opened",
322
+ "v": 1,
323
+ "api": "github/2022-11-28",
324
+ "entity": "event:x:y",
325
+ "at": "2026-10-02T10:30:00Z",
326
+ "payload": {}
327
+ },
328
+ "isValid": false
329
+ },
330
+ {
331
+ "title": "refuses an actor that is an event fact ID",
332
+ "envelope": {
333
+ "id": "webhook:github:d-1",
334
+ "type": "github.pull_request.opened",
335
+ "v": 1,
336
+ "api": "github/2022-11-28",
337
+ "entity": "github:pull_request:77",
338
+ "at": "2026-10-02T10:30:00Z",
339
+ "payload": {},
340
+ "actor": "event:boxxy:evt-0001"
341
+ },
342
+ "isValid": false
343
+ },
344
+ {
345
+ "title": "accepts an entity ID whose source only starts with a fact kind",
346
+ "envelope": {
347
+ "id": "webhook:github:d-1",
348
+ "type": "github.pull_request.opened",
349
+ "v": 1,
350
+ "api": "github/2022-11-28",
351
+ "entity": "events:x:y",
352
+ "at": "2026-10-02T10:30:00Z",
353
+ "payload": {}
354
+ },
355
+ "isValid": true
356
+ },
357
+ {
358
+ "title": "refuses an actor that is not an entity ID",
359
+ "envelope": {
360
+ "id": "webhook:github:d-1",
361
+ "type": "github.pull_request.opened",
362
+ "v": 1,
363
+ "api": "github/2022-11-28",
364
+ "entity": "github:pull_request:77",
365
+ "at": "2026-10-02T10:30:00Z",
366
+ "payload": {},
367
+ "actor": "github:7"
368
+ },
369
+ "isValid": false
370
+ },
371
+ {
372
+ "title": "refuses a time with no seconds",
373
+ "envelope": {
374
+ "id": "webhook:github:d-1",
375
+ "type": "github.pull_request.opened",
376
+ "v": 1,
377
+ "api": "github/2022-11-28",
378
+ "entity": "github:pull_request:77",
379
+ "at": "2026-10-02T10:30Z",
380
+ "payload": {}
381
+ },
382
+ "isValid": false
383
+ },
384
+ {
385
+ "title": "refuses a time with an offset",
386
+ "envelope": {
387
+ "id": "webhook:github:d-1",
388
+ "type": "github.pull_request.opened",
389
+ "v": 1,
390
+ "api": "github/2022-11-28",
391
+ "entity": "github:pull_request:77",
392
+ "at": "2026-10-02T10:30:00+01:00",
393
+ "payload": {}
394
+ },
395
+ "isValid": false
396
+ },
397
+ {
398
+ "title": "refuses a day the calendar lacks",
399
+ "envelope": {
400
+ "id": "webhook:github:d-1",
401
+ "type": "github.pull_request.opened",
402
+ "v": 1,
403
+ "api": "github/2022-11-28",
404
+ "entity": "github:pull_request:77",
405
+ "at": "2026-02-30T10:30:00Z",
406
+ "payload": {}
407
+ },
408
+ "isValid": false
409
+ },
410
+ {
411
+ "title": "refuses the hour 24",
412
+ "envelope": {
413
+ "id": "webhook:github:d-1",
414
+ "type": "github.pull_request.opened",
415
+ "v": 1,
416
+ "api": "github/2022-11-28",
417
+ "entity": "github:pull_request:77",
418
+ "at": "2026-10-02T24:00:00Z",
419
+ "payload": {}
420
+ },
421
+ "isValid": false
422
+ },
423
+ {
424
+ "title": "refuses a cursor that is not text",
425
+ "envelope": {
426
+ "id": "webhook:github:d-1",
427
+ "type": "github.pull_request.opened",
428
+ "v": 1,
429
+ "api": "github/2022-11-28",
430
+ "entity": "github:pull_request:77",
431
+ "at": "2026-10-02T10:30:00Z",
432
+ "payload": {},
433
+ "cursor": 3
434
+ },
435
+ "isValid": false
436
+ },
437
+ {
438
+ "title": "refuses a field the envelope lacks",
439
+ "envelope": {
440
+ "id": "webhook:github:d-1",
441
+ "type": "github.pull_request.opened",
442
+ "v": 1,
443
+ "api": "github/2022-11-28",
444
+ "entity": "github:pull_request:77",
445
+ "at": "2026-10-02T10:30:00Z",
446
+ "payload": {},
447
+ "extra": 1
448
+ },
449
+ "isValid": false
450
+ },
451
+ {
452
+ "title": "refuses an envelope without a payload",
453
+ "envelope": {
454
+ "id": "webhook:github:d-1",
455
+ "type": "github.pull_request.opened",
456
+ "v": 1,
457
+ "api": "github/2022-11-28",
458
+ "entity": "github:pull_request:77",
459
+ "at": "2026-10-02T10:30:00Z"
460
+ },
461
+ "isValid": false
462
+ }
463
+ ]
464
+ }
@@ -0,0 +1,15 @@
1
+ import { toTagged } from '@zgeoff/std';
2
+ import type { FactID } from '../ids/types.ts';
3
+ import { buildFactHash } from './build-fact-hash.ts';
4
+
5
+ interface EnrichmentFactIDParts {
6
+ job: string;
7
+ jobVersion: string;
8
+ inputHash: string;
9
+ }
10
+
11
+ export function buildEnrichmentFactID(parts: EnrichmentFactIDParts): FactID {
12
+ const hash = buildFactHash([parts.job, parts.jobVersion, parts.inputHash]);
13
+
14
+ return toTagged<FactID>(`enrichment:${hash}`);
15
+ }
@@ -0,0 +1,11 @@
1
+ import { toTagged } from '@zgeoff/std';
2
+ import type { FactID } from '../ids/types.ts';
3
+
4
+ interface EventFactIDParts {
5
+ source: string;
6
+ event: string;
7
+ }
8
+
9
+ export function buildEventFactID(parts: EventFactIDParts): FactID {
10
+ return toTagged<FactID>(`event:${parts.source}:${parts.event}`);
11
+ }
@@ -0,0 +1,6 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ export function buildFactHash(fields: readonly string[]): string {
4
+ // A JSON array keeps each field's boundary, which a delimiter-joined string would let a field forge.
5
+ return createHash('sha256').update(JSON.stringify(fields)).digest('hex');
6
+ }
@@ -0,0 +1,7 @@
1
+ import { toTagged } from '@zgeoff/std';
2
+ import type { ClientID, FactID, RequestID } from '../ids/types.ts';
3
+ import { buildFactHash } from './build-fact-hash.ts';
4
+
5
+ export function buildManualFactID(client: ClientID, request: RequestID): FactID {
6
+ return toTagged<FactID>(`manual:${buildFactHash([client, request])}`);
7
+ }
@@ -0,0 +1,15 @@
1
+ import { toTagged } from '@zgeoff/std';
2
+ import type { EntityID, FactID } from '../ids/types.ts';
3
+ import { buildFactHash } from './build-fact-hash.ts';
4
+
5
+ interface ObservationFactIDParts {
6
+ source: string;
7
+ entity: EntityID;
8
+ sourceUpdateTime: string;
9
+ }
10
+
11
+ export function buildObservationFactID(parts: ObservationFactIDParts): FactID {
12
+ const hash = buildFactHash([parts.source, parts.entity, parts.sourceUpdateTime]);
13
+
14
+ return toTagged<FactID>(`observation:${hash}`);
15
+ }
@@ -0,0 +1,11 @@
1
+ import { toTagged } from '@zgeoff/std';
2
+ import type { FactID } from '../ids/types.ts';
3
+
4
+ interface WebhookFactIDParts {
5
+ source: string;
6
+ deliveryID: string;
7
+ }
8
+
9
+ export function buildWebhookFactID(parts: WebhookFactIDParts): FactID {
10
+ return toTagged<FactID>(`webhook:${parts.source}:${parts.deliveryID}`);
11
+ }
@@ -0,0 +1,44 @@
1
+ import { toTagged } from '@zgeoff/std';
2
+ import { z } from 'zod';
3
+ import { parseEntityID } from '../entity-ids/parse-entity-id.ts';
4
+ import type { EntityID, FactID } from '../ids/types.ts';
5
+ import type { FactEnvelope, Result } from '../types.ts';
6
+
7
+ const factIDPattern = new RegExp(
8
+ ['^(?:webhook|event):[a-z][a-z0-9_]*:.+$', '^(?:observation|enrichment|manual):[0-9a-f]{64}$']
9
+ .map((pattern) => `(?:${pattern})`)
10
+ .join('|'),
11
+ 'u',
12
+ );
13
+
14
+ const factIDSchema = z
15
+ .string()
16
+ .regex(factIDPattern, 'not a fact ID')
17
+ .transform((text) => toTagged<FactID>(text));
18
+
19
+ const entityIDSchema = z
20
+ .string()
21
+ .refine((text) => parseEntityID(text).ok, 'not an entity ID')
22
+ .transform((text) => toTagged<EntityID>(text));
23
+
24
+ const factEnvelopeSchema = z.strictObject({
25
+ id: factIDSchema,
26
+ type: z.string().min(1),
27
+ v: z.literal(1),
28
+ api: z.string().min(1),
29
+ entity: entityIDSchema,
30
+ actor: entityIDSchema.optional(),
31
+ at: z.iso.datetime(),
32
+ cursor: z.string().optional(),
33
+ payload: z.unknown().refine((payload) => payload !== undefined, 'a fact needs a payload'),
34
+ });
35
+
36
+ export function parseFactEnvelope(input: unknown): Result<FactEnvelope<unknown>, z.ZodError> {
37
+ const parsed = factEnvelopeSchema.safeParse(input);
38
+
39
+ if (!parsed.success) {
40
+ return { ok: false, failure: parsed.error };
41
+ }
42
+
43
+ return { ok: true, value: parsed.data };
44
+ }
@@ -0,0 +1,33 @@
1
+ import type { Result } from '../types.ts';
2
+
3
+ const partPattern = /^[a-z][a-z0-9_]*$/u;
4
+
5
+ interface StreamNameParts {
6
+ source: string;
7
+ family: string;
8
+ }
9
+
10
+ type StreamNameFailure =
11
+ | { reason: 'malformed' }
12
+ | { reason: 'invalid-source' }
13
+ | { reason: 'invalid-family' };
14
+
15
+ export function parseStreamName(text: string): Result<StreamNameParts, StreamNameFailure> {
16
+ const parts = text.split('.');
17
+
18
+ if (parts.length !== 3 || parts[0] !== 'facts') {
19
+ return { ok: false, failure: { reason: 'malformed' } };
20
+ }
21
+
22
+ const [, source = '', family = ''] = parts;
23
+
24
+ if (!partPattern.test(source)) {
25
+ return { ok: false, failure: { reason: 'invalid-source' } };
26
+ }
27
+
28
+ if (!partPattern.test(family)) {
29
+ return { ok: false, failure: { reason: 'invalid-family' } };
30
+ }
31
+
32
+ return { ok: true, value: { source, family } };
33
+ }