@bjornpagen/bumbledb-log 0.17.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.
@@ -0,0 +1,741 @@
1
+ /**
2
+ * The parsed protocol descriptor: the SDK's lowered `SchemaSpec` resolved
3
+ * once — names to dense ids, handles to row ids, statements materialized
4
+ * in the engine's own order (fresh auto-keys first, closed auto-keys,
5
+ * then declared statements with `==` split into two containments) — so
6
+ * every pure function downstream reads ids, never names. Braid
7
+ * derivation and the schema fingerprint mirror live on the same parse:
8
+ * one boundary, parsed in full, cached per theory value.
9
+ */
10
+
11
+ import type {
12
+ AnySchema,
13
+ LiteralSetSpec,
14
+ LiteralSpec,
15
+ SchemaSpec,
16
+ SideSpec,
17
+ StatementSpec,
18
+ ValueSpec,
19
+ ValueTypeSpec
20
+ } from "@bjornpagen/bumbledb"
21
+ import { internalBlake3, lower } from "@bjornpagen/bumbledb"
22
+ import * as errors from "@superbuilders/errors"
23
+ import { ByteWriter, fromHex, toHex, utf8Encoder } from "#bytes.ts"
24
+ import type { LogValue } from "#value.ts"
25
+ import { writeCanonicalLiteral } from "#value.ts"
26
+
27
+ interface FieldInfo {
28
+ readonly name: string
29
+ readonly type: ValueTypeSpec
30
+ readonly fresh: boolean
31
+ /** Set when the field's newtype names a closed relation's id class. */
32
+ readonly closedRef: string | undefined
33
+ }
34
+
35
+ interface RelationInfo {
36
+ readonly id: number
37
+ readonly name: string
38
+ readonly closed: boolean
39
+ readonly handles: readonly string[]
40
+ /** Sealed order: a closed relation's synthetic u64 `id` at ordinal 0. */
41
+ readonly fields: readonly FieldInfo[]
42
+ /** Closed ground axioms in sealed order (id first), resolved values. */
43
+ readonly rows: ReadonlyArray<readonly LogValue[]>
44
+ }
45
+
46
+ interface SideInfo {
47
+ readonly relation: number
48
+ readonly projection: readonly number[]
49
+ readonly selection: ReadonlyArray<{ readonly field: number; readonly values: readonly LogValue[] }>
50
+ }
51
+
52
+ type WeightInfo =
53
+ | { readonly kind: "unit" }
54
+ | { readonly kind: "field"; readonly field: number }
55
+ | { readonly kind: "duration"; readonly field: number }
56
+
57
+ type HiInfo =
58
+ | { readonly kind: "unbounded" }
59
+ | { readonly kind: "lit"; readonly value: bigint }
60
+ | { readonly kind: "targetField"; readonly field: number }
61
+ | { readonly kind: "targetDuration"; readonly field: number }
62
+
63
+ type StatementInfo =
64
+ | {
65
+ readonly id: number
66
+ readonly kind: "functionality"
67
+ readonly relation: number
68
+ readonly projection: readonly number[]
69
+ }
70
+ | { readonly id: number; readonly kind: "containment"; readonly source: SideInfo; readonly target: SideInfo }
71
+ | {
72
+ readonly id: number
73
+ readonly kind: "capacity"
74
+ readonly target: SideInfo
75
+ readonly weight: WeightInfo
76
+ readonly lo: bigint
77
+ readonly hi: HiInfo
78
+ readonly source: SideInfo
79
+ }
80
+
81
+ interface SerialStatement {
82
+ readonly statement: number
83
+ readonly braid: string
84
+ }
85
+
86
+ interface LogDescriptor {
87
+ readonly relations: readonly RelationInfo[]
88
+ readonly relationByName: ReadonlyMap<string, RelationInfo>
89
+ readonly statements: readonly StatementInfo[]
90
+ /** Ordinary relation id → braid id string (`c{smallest:08x}`). */
91
+ readonly braidOfRelation: ReadonlyMap<number, string>
92
+ /** Braid id string → member relation ids, ascending. */
93
+ readonly braidMembers: ReadonlyMap<string, readonly number[]>
94
+ readonly serialAtStatements: readonly SerialStatement[]
95
+ readonly fingerprint: string
96
+ readonly fingerprintBytes: Uint8Array
97
+ }
98
+
99
+ /** The pure trio's input: the theory value, its lowered spec, or an already-parsed descriptor. */
100
+ type LogTheory = AnySchema | SchemaSpec | LogDescriptor
101
+
102
+ function isDescriptor(theory: LogTheory): theory is LogDescriptor {
103
+ return "braidMembers" in theory
104
+ }
105
+
106
+ function isSpec(theory: LogTheory): theory is SchemaSpec {
107
+ return Array.isArray((theory as SchemaSpec).relations)
108
+ }
109
+
110
+ const cache = new WeakMap<object, LogDescriptor>()
111
+
112
+ function descriptorOf(theory: LogTheory): LogDescriptor {
113
+ if (isDescriptor(theory)) {
114
+ return theory
115
+ }
116
+ const hit = cache.get(theory)
117
+ if (hit !== undefined) {
118
+ return hit
119
+ }
120
+ const spec = isSpec(theory) ? theory : lower(theory)
121
+ const parsed = spec === theory ? parseSpec(spec) : (cache.get(spec) ?? parseSpec(spec))
122
+ cache.set(theory, parsed)
123
+ cache.set(spec, parsed)
124
+ return parsed
125
+ }
126
+
127
+ function braidHex(relationId: number): string {
128
+ return `c${relationId.toString(16).padStart(8, "0")}`
129
+ }
130
+
131
+ function logValueOf(value: ValueSpec): LogValue {
132
+ switch (value.kind) {
133
+ case "bool":
134
+ return value.value
135
+ case "u64":
136
+ case "i64":
137
+ return value.value
138
+ case "string":
139
+ return value.value
140
+ case "fixedBytes":
141
+ return value.value
142
+ case "intervalU64":
143
+ case "intervalI64":
144
+ return { start: value.start, end: value.end }
145
+ }
146
+ }
147
+
148
+ interface SpecTables {
149
+ readonly spec: SchemaSpec
150
+ readonly relations: RelationInfo[]
151
+ readonly byName: Map<string, RelationInfo>
152
+ }
153
+
154
+ function fieldsOf(
155
+ tables: Map<string, { closed: boolean; handles: readonly string[] }>,
156
+ relation: SchemaSpec["relations"][number]
157
+ ): FieldInfo[] {
158
+ const fields: FieldInfo[] = []
159
+ if (relation.closed !== undefined) {
160
+ fields.push({
161
+ name: "id",
162
+ type: { kind: "u64" },
163
+ fresh: false,
164
+ closedRef: relation.name
165
+ })
166
+ }
167
+ for (const field of relation.fields) {
168
+ let closedRef: string | undefined
169
+ if (field.newtype?.endsWith(".id")) {
170
+ const owner = field.newtype.slice(0, -3)
171
+ const target = tables.get(owner)
172
+ if (target?.closed === true) {
173
+ closedRef = owner
174
+ }
175
+ }
176
+ fields.push({ name: field.name, type: field.valueType, fresh: field.fresh, closedRef })
177
+ }
178
+ return fields
179
+ }
180
+
181
+ function resolveLiteral(tables: SpecTables, relation: RelationInfo, field: FieldInfo, literal: LiteralSpec): LogValue {
182
+ if (literal.kind === "value") {
183
+ return logValueOf(literal.value)
184
+ }
185
+ if (field.closedRef === undefined) {
186
+ throw errors.new(
187
+ `handle literal ${literal.handle} on ${relation.name}.${field.name}, which references no closed roster`
188
+ )
189
+ }
190
+ const target = tables.byName.get(field.closedRef)
191
+ if (target === undefined) {
192
+ throw errors.new(`handle literal ${literal.handle}: unknown closed relation ${field.closedRef}`)
193
+ }
194
+ const id = target.handles.indexOf(literal.handle)
195
+ if (id === -1) {
196
+ throw errors.new(`handle literal ${literal.handle} is not in the ${target.name} roster`)
197
+ }
198
+ return BigInt(id)
199
+ }
200
+
201
+ function literalSetOf(tables: SpecTables, relation: RelationInfo, field: FieldInfo, set: LiteralSetSpec): LogValue[] {
202
+ if (set.kind === "one") {
203
+ return [resolveLiteral(tables, relation, field, set.literal)]
204
+ }
205
+ return set.literals.map(function resolveEach(literal) {
206
+ return resolveLiteral(tables, relation, field, literal)
207
+ })
208
+ }
209
+
210
+ function fieldOrdinal(relation: RelationInfo, name: string): number {
211
+ const ordinal = relation.fields.findIndex(function byName(field) {
212
+ return field.name === name
213
+ })
214
+ if (ordinal === -1) {
215
+ throw errors.new(`relation ${relation.name} has no field ${name}`)
216
+ }
217
+ return ordinal
218
+ }
219
+
220
+ function sideOf(tables: SpecTables, side: SideSpec): SideInfo {
221
+ const relation = tables.byName.get(side.relation)
222
+ if (relation === undefined) {
223
+ throw errors.new(`statement cites unknown relation ${side.relation}`)
224
+ }
225
+ const projection = side.projection.map(function ordinalOf(name) {
226
+ return fieldOrdinal(relation, name)
227
+ })
228
+ const selection = side.selection.map(function bindingOf(binding) {
229
+ const ordinal = fieldOrdinal(relation, binding[0])
230
+ const field = relation.fields[ordinal]
231
+ if (field === undefined) {
232
+ throw errors.new(`relation ${relation.name} has no field ordinal ${ordinal}`)
233
+ }
234
+ return { field: ordinal, values: literalSetOf(tables, relation, field, binding[1]) }
235
+ })
236
+ return { relation: relation.id, projection, selection }
237
+ }
238
+
239
+ function boundValue(context: string, bound: { readonly kind: string }): bigint {
240
+ if (bound.kind !== "lit") {
241
+ throw errors.new(`${context}: dependent floors are refused by the schema grammar`)
242
+ }
243
+ return (bound as { readonly kind: "lit"; readonly value: bigint }).value
244
+ }
245
+
246
+ function capacityOf(
247
+ tables: SpecTables,
248
+ id: number,
249
+ statement: Extract<StatementSpec, { kind: "capacity" }>
250
+ ): StatementInfo {
251
+ const target = sideOf(tables, statement.target)
252
+ const source = sideOf(tables, statement.source)
253
+ const sourceRelation = tables.byName.get(statement.source.relation)
254
+ const targetRelation = tables.byName.get(statement.target.relation)
255
+ if (sourceRelation === undefined || targetRelation === undefined) {
256
+ throw errors.new("capacity statement cites unknown relations")
257
+ }
258
+ let weight: WeightInfo
259
+ switch (statement.weight.kind) {
260
+ case "unit": {
261
+ weight = { kind: "unit" }
262
+ break
263
+ }
264
+ case "field": {
265
+ weight = { kind: "field", field: fieldOrdinal(sourceRelation, statement.weight.field) }
266
+ break
267
+ }
268
+ case "durationField": {
269
+ weight = { kind: "duration", field: fieldOrdinal(sourceRelation, statement.weight.field) }
270
+ break
271
+ }
272
+ }
273
+ let lo: bigint
274
+ let hi: HiInfo
275
+ const window = statement.window
276
+ switch (window.kind) {
277
+ case "exact": {
278
+ lo = boundValue("capacity exact bound", window.n)
279
+ hi = { kind: "lit", value: lo }
280
+ break
281
+ }
282
+ case "floor": {
283
+ lo = boundValue("capacity floor", window.lo)
284
+ hi = { kind: "unbounded" }
285
+ break
286
+ }
287
+ case "range": {
288
+ lo = boundValue("capacity floor", window.lo)
289
+ switch (window.hi.kind) {
290
+ case "lit": {
291
+ hi = { kind: "lit", value: window.hi.value }
292
+ break
293
+ }
294
+ case "field": {
295
+ hi = { kind: "targetField", field: fieldOrdinal(targetRelation, window.hi.field) }
296
+ break
297
+ }
298
+ case "durationField": {
299
+ hi = { kind: "targetDuration", field: fieldOrdinal(targetRelation, window.hi.field) }
300
+ break
301
+ }
302
+ }
303
+ break
304
+ }
305
+ }
306
+ return { id, kind: "capacity", target, weight, lo, hi, source }
307
+ }
308
+
309
+ /** The `exact {n}` window: lo = hi = n; a non-literal n is grammar-refused upstream. */
310
+
311
+ function parseSpec(spec: SchemaSpec): LogDescriptor {
312
+ const prepass = new Map<string, { closed: boolean; handles: readonly string[] }>()
313
+ for (const relation of spec.relations) {
314
+ prepass.set(relation.name, {
315
+ closed: relation.closed !== undefined,
316
+ handles: relation.closed === undefined ? [] : relation.closed.rows.map((row) => row.handle)
317
+ })
318
+ }
319
+
320
+ const relations: RelationInfo[] = []
321
+ const byName = new Map<string, RelationInfo>()
322
+ spec.relations.forEach(function buildRelation(relation, id) {
323
+ const fields = fieldsOf(prepass, relation)
324
+ const info: RelationInfo = {
325
+ id,
326
+ name: relation.name,
327
+ closed: relation.closed !== undefined,
328
+ handles: prepass.get(relation.name)?.handles ?? [],
329
+ fields,
330
+ rows: []
331
+ }
332
+ relations.push(info)
333
+ byName.set(relation.name, info)
334
+ })
335
+ const tables: SpecTables = { spec, relations, byName }
336
+
337
+ spec.relations.forEach(function resolveRows(relation, id) {
338
+ if (relation.closed === undefined) {
339
+ return
340
+ }
341
+ const info = relations[id]
342
+ if (info === undefined) {
343
+ throw errors.new(`relation ordinal ${id} missing`)
344
+ }
345
+ const rows = relation.closed.rows.map(function resolveRow(row, rowId) {
346
+ const values: LogValue[] = [BigInt(rowId)]
347
+ row.values.forEach(function resolveCell(literal, column) {
348
+ const field = info.fields[column + 1]
349
+ if (field === undefined) {
350
+ throw errors.new(`closed row ${row.handle} of ${relation.name}: no field at column ${column}`)
351
+ }
352
+ values.push(resolveLiteral(tables, info, field, literal))
353
+ })
354
+ return values
355
+ })
356
+ relations[id] = { ...info, rows }
357
+ byName.set(relation.name, relations[id])
358
+ })
359
+
360
+ const statements: StatementInfo[] = []
361
+ relations.forEach(function freshKeys(relation) {
362
+ relation.fields.forEach(function freshKey(field, ordinal) {
363
+ if (field.fresh) {
364
+ statements.push({ id: statements.length, kind: "functionality", relation: relation.id, projection: [ordinal] })
365
+ }
366
+ })
367
+ })
368
+ relations.forEach(function closedKeys(relation) {
369
+ if (relation.closed) {
370
+ statements.push({ id: statements.length, kind: "functionality", relation: relation.id, projection: [0] })
371
+ }
372
+ })
373
+ for (const statement of spec.statements) {
374
+ switch (statement.kind) {
375
+ case "fd": {
376
+ const relation = byName.get(statement.relation)
377
+ if (relation === undefined) {
378
+ throw errors.new(`key statement cites unknown relation ${statement.relation}`)
379
+ }
380
+ statements.push({
381
+ id: statements.length,
382
+ kind: "functionality",
383
+ relation: relation.id,
384
+ projection: statement.projection.map(function ordinalOf(name) {
385
+ return fieldOrdinal(relation, name)
386
+ })
387
+ })
388
+ break
389
+ }
390
+ case "containment": {
391
+ const source = sideOf(tables, statement.source)
392
+ const target = sideOf(tables, statement.target)
393
+ statements.push({ id: statements.length, kind: "containment", source, target })
394
+ if (statement.bidirectional) {
395
+ statements.push({ id: statements.length, kind: "containment", source: target, target: source })
396
+ }
397
+ break
398
+ }
399
+ case "capacity": {
400
+ statements.push(capacityOf(tables, statements.length, statement))
401
+ break
402
+ }
403
+ }
404
+ }
405
+
406
+ const { braidOfRelation, braidMembers } = deriveBraids(relations, statements)
407
+
408
+ const serialAtStatements: SerialStatement[] = []
409
+ for (const statement of statements) {
410
+ if (statement.kind === "functionality" && statement.projection.length === 0) {
411
+ const braid = braidOfRelation.get(statement.relation)
412
+ if (braid !== undefined) {
413
+ serialAtStatements.push({ statement: statement.id, braid })
414
+ }
415
+ }
416
+ if (statement.kind === "capacity" && statement.target.projection.length === 0) {
417
+ const targetRelation = relations[statement.target.relation]
418
+ if (targetRelation !== undefined && !targetRelation.closed) {
419
+ const braid = braidOfRelation.get(statement.target.relation)
420
+ if (braid !== undefined) {
421
+ serialAtStatements.push({ statement: statement.id, braid })
422
+ }
423
+ }
424
+ }
425
+ }
426
+
427
+ let hashed: { readonly hex: string; readonly bytes: Uint8Array } | undefined
428
+ function fingerprintLazily(): { readonly hex: string; readonly bytes: Uint8Array } {
429
+ if (hashed === undefined) {
430
+ const bytes = fingerprintOf(relations, statements)
431
+ hashed = { hex: toHex(bytes), bytes }
432
+ }
433
+ return hashed
434
+ }
435
+
436
+ const descriptor: LogDescriptor = {
437
+ relations,
438
+ relationByName: byName,
439
+ statements,
440
+ braidOfRelation,
441
+ braidMembers,
442
+ serialAtStatements,
443
+ get fingerprint() {
444
+ return fingerprintLazily().hex
445
+ },
446
+ get fingerprintBytes() {
447
+ return fingerprintLazily().bytes
448
+ }
449
+ }
450
+ return descriptor
451
+ }
452
+
453
+ /**
454
+ * The same descriptor under a pinned fingerprint — for stores whose
455
+ * identity is carried (a manifest, a conformance sidecar) rather than
456
+ * recomputed, e.g. when the mirror cannot hash a closed relation's
457
+ * interned string axioms.
458
+ */
459
+ function withFingerprint(descriptor: LogDescriptor, fingerprint: string): LogDescriptor {
460
+ return {
461
+ relations: descriptor.relations,
462
+ relationByName: descriptor.relationByName,
463
+ statements: descriptor.statements,
464
+ braidOfRelation: descriptor.braidOfRelation,
465
+ braidMembers: descriptor.braidMembers,
466
+ serialAtStatements: descriptor.serialAtStatements,
467
+ fingerprint,
468
+ fingerprintBytes: fromHex(fingerprint)
469
+ }
470
+ }
471
+
472
+ function deriveBraids(
473
+ relations: readonly RelationInfo[],
474
+ statements: readonly StatementInfo[]
475
+ ): { braidOfRelation: Map<number, string>; braidMembers: Map<string, readonly number[]> } {
476
+ const parent = new Map<number, number>()
477
+ for (const relation of relations) {
478
+ if (!relation.closed) {
479
+ parent.set(relation.id, relation.id)
480
+ }
481
+ }
482
+ function rootOf(id: number): number {
483
+ let cursor = id
484
+ for (;;) {
485
+ const up = parent.get(cursor)
486
+ if (up === undefined || up === cursor) {
487
+ return cursor
488
+ }
489
+ cursor = up
490
+ }
491
+ }
492
+ function union(a: number, b: number): void {
493
+ const ra = rootOf(a)
494
+ const rb = rootOf(b)
495
+ if (ra !== rb) {
496
+ parent.set(Math.max(ra, rb), Math.min(ra, rb))
497
+ }
498
+ }
499
+ for (const statement of statements) {
500
+ if (statement.kind === "functionality") {
501
+ continue
502
+ }
503
+ const source = relations[statement.source.relation]
504
+ const target = relations[statement.target.relation]
505
+ if (source === undefined || target === undefined || source.closed || target.closed) {
506
+ continue
507
+ }
508
+ union(source.id, target.id)
509
+ }
510
+ const members = new Map<number, number[]>()
511
+ for (const id of parent.keys()) {
512
+ const root = rootOf(id)
513
+ const list = members.get(root)
514
+ if (list === undefined) {
515
+ members.set(root, [id])
516
+ } else {
517
+ list.push(id)
518
+ }
519
+ }
520
+ const braidOfRelation = new Map<number, string>()
521
+ const braidMembers = new Map<string, readonly number[]>()
522
+ for (const [root, ids] of [...members.entries()].sort(function byRoot(a, b) {
523
+ return a[0] - b[0]
524
+ })) {
525
+ const braid = braidHex(root)
526
+ ids.sort(function ascending(a, b) {
527
+ return a - b
528
+ })
529
+ braidMembers.set(braid, ids)
530
+ for (const id of ids) {
531
+ braidOfRelation.set(id, braid)
532
+ }
533
+ }
534
+ return { braidOfRelation, braidMembers }
535
+ }
536
+
537
+ /** `bumbledb-schema-v5` canonical bytes, mirrored from the engine's own encoder. */
538
+ const FORMAT_VERSION_LABEL = "bumbledb-schema-v5"
539
+
540
+ const VALUE_TYPE_TAG = { bool: 0, u64: 2, i64: 3, string: 4, fixedBytes: 5, interval: 6, fixedInterval: 7 } as const
541
+
542
+ function putLen(out: ByteWriter, len: number): void {
543
+ out.u32le(len)
544
+ }
545
+
546
+ function putBytes(out: ByteWriter, raw: Uint8Array): void {
547
+ putLen(out, raw.length)
548
+ out.bytes(raw)
549
+ }
550
+
551
+ function putString(out: ByteWriter, text: string): void {
552
+ putBytes(out, utf8Encoder.encode(text))
553
+ }
554
+
555
+ function putValueType(out: ByteWriter, type: ValueTypeSpec): void {
556
+ switch (type.kind) {
557
+ case "bool": {
558
+ out.u8(VALUE_TYPE_TAG.bool)
559
+ return
560
+ }
561
+ case "u64": {
562
+ out.u8(VALUE_TYPE_TAG.u64)
563
+ return
564
+ }
565
+ case "i64": {
566
+ out.u8(VALUE_TYPE_TAG.i64)
567
+ return
568
+ }
569
+ case "string": {
570
+ out.u8(VALUE_TYPE_TAG.string)
571
+ return
572
+ }
573
+ case "fixedBytes": {
574
+ out.u8(VALUE_TYPE_TAG.fixedBytes)
575
+ out.u16le(type.len)
576
+ return
577
+ }
578
+ case "interval": {
579
+ if (type.width === undefined) {
580
+ out.u8(VALUE_TYPE_TAG.interval)
581
+ out.u8(type.element === "u64" ? 0 : 1)
582
+ return
583
+ }
584
+ out.u8(VALUE_TYPE_TAG.fixedInterval)
585
+ out.u8(type.element === "u64" ? 0 : 1)
586
+ out.u64le(type.width)
587
+ return
588
+ }
589
+ }
590
+ }
591
+
592
+ function putSide(out: ByteWriter, relations: readonly RelationInfo[], side: SideInfo): void {
593
+ out.u32le(side.relation)
594
+ putLen(out, side.projection.length)
595
+ for (const field of side.projection) {
596
+ out.u16le(field)
597
+ }
598
+ putLen(out, side.selection.length)
599
+ const relation = relations[side.relation]
600
+ if (relation === undefined) {
601
+ throw errors.new(`side cites unknown relation id ${side.relation}`)
602
+ }
603
+ for (const binding of side.selection) {
604
+ out.u16le(binding.field)
605
+ const field = relation.fields[binding.field]
606
+ if (field === undefined) {
607
+ throw errors.new(`selection cites unknown field ordinal ${binding.field}`)
608
+ }
609
+ putLen(out, binding.values.length)
610
+ for (const value of binding.values) {
611
+ if (typeof value === "string") {
612
+ putString(out, value)
613
+ } else {
614
+ writeCanonicalLiteral(out, field.type, value)
615
+ }
616
+ }
617
+ }
618
+ }
619
+
620
+ function fingerprintOf(relations: readonly RelationInfo[], statements: readonly StatementInfo[]): Uint8Array {
621
+ const out = new ByteWriter(1024)
622
+ putString(out, FORMAT_VERSION_LABEL)
623
+ putLen(out, relations.length)
624
+ for (const relation of relations) {
625
+ putString(out, relation.name)
626
+ putLen(out, relation.fields.length)
627
+ for (const field of relation.fields) {
628
+ putString(out, field.name)
629
+ putValueType(out, field.type)
630
+ out.u8(field.fresh ? 1 : 0)
631
+ }
632
+ if (!relation.closed) {
633
+ out.u8(0)
634
+ } else {
635
+ out.u8(1)
636
+ putLen(out, relation.rows.length)
637
+ relation.rows.forEach(function putRow(row, rowId) {
638
+ const handle = relation.handles[rowId]
639
+ if (handle === undefined) {
640
+ throw errors.new(`closed relation ${relation.name}: no handle for row ${rowId}`)
641
+ }
642
+ putString(out, handle)
643
+ const fact = new ByteWriter(64)
644
+ row.forEach(function putCell(value, ordinal) {
645
+ const field = relation.fields[ordinal]
646
+ if (field === undefined) {
647
+ throw errors.new(`closed relation ${relation.name}: no field at ordinal ${ordinal}`)
648
+ }
649
+ if (field.type.kind === "string") {
650
+ throw errors.new(
651
+ `closed relation ${relation.name} has a string ground axiom — the fingerprint mirror does not carry interned axiom columns`
652
+ )
653
+ }
654
+ writeCanonicalLiteral(fact, field.type, value)
655
+ })
656
+ putBytes(out, fact.finish())
657
+ })
658
+ }
659
+ }
660
+ putLen(out, statements.length)
661
+ for (const statement of statements) {
662
+ switch (statement.kind) {
663
+ case "functionality": {
664
+ out.u8(0)
665
+ out.u32le(statement.relation)
666
+ putLen(out, statement.projection.length)
667
+ for (const field of statement.projection) {
668
+ out.u16le(field)
669
+ }
670
+ break
671
+ }
672
+ case "containment": {
673
+ out.u8(1)
674
+ putSide(out, relations, statement.source)
675
+ putSide(out, relations, statement.target)
676
+ break
677
+ }
678
+ case "capacity": {
679
+ out.u8(4)
680
+ putSide(out, relations, statement.target)
681
+ switch (statement.weight.kind) {
682
+ case "unit": {
683
+ out.u8(0)
684
+ break
685
+ }
686
+ case "field": {
687
+ out.u8(1)
688
+ out.u16le(statement.weight.field)
689
+ break
690
+ }
691
+ case "duration": {
692
+ out.u8(2)
693
+ out.u16le(statement.weight.field)
694
+ break
695
+ }
696
+ }
697
+ out.u64le(statement.lo)
698
+ switch (statement.hi.kind) {
699
+ case "unbounded": {
700
+ out.u8(0)
701
+ break
702
+ }
703
+ case "lit": {
704
+ out.u8(1)
705
+ out.u8(0)
706
+ out.u64le(statement.hi.value)
707
+ break
708
+ }
709
+ case "targetField": {
710
+ out.u8(1)
711
+ out.u8(1)
712
+ out.u16le(statement.hi.field)
713
+ break
714
+ }
715
+ case "targetDuration": {
716
+ out.u8(1)
717
+ out.u8(2)
718
+ out.u16le(statement.hi.field)
719
+ break
720
+ }
721
+ }
722
+ putSide(out, relations, statement.source)
723
+ break
724
+ }
725
+ }
726
+ }
727
+ return new Uint8Array(internalBlake3(out.finish()))
728
+ }
729
+
730
+ export type {
731
+ FieldInfo,
732
+ HiInfo,
733
+ LogDescriptor,
734
+ LogTheory,
735
+ RelationInfo,
736
+ SerialStatement,
737
+ SideInfo,
738
+ StatementInfo,
739
+ WeightInfo
740
+ }
741
+ export { braidHex, descriptorOf, withFingerprint }