@bjornpagen/bumbledb-log 0.17.0 → 0.19.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/src/descriptor.ts CHANGED
@@ -1,11 +1,8 @@
1
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.
2
+ * The parsed protocol descriptor: one thin parse of the engine's sealed
3
+ * truth — relation ids, sealed field order, resolved closed rows,
4
+ * materialized statements, and the real fingerprint — plus the braid
5
+ * map the protocol derives from that truth. Cached per theory value.
9
6
  */
10
7
 
11
8
  import type {
@@ -13,20 +10,22 @@ import type {
13
10
  LiteralSetSpec,
14
11
  LiteralSpec,
15
12
  SchemaSpec,
13
+ SealedDescriptor,
14
+ SealedSide,
15
+ SealedStatement,
16
16
  SideSpec,
17
17
  StatementSpec,
18
- ValueSpec,
19
- ValueTypeSpec
18
+ ValueSpec
20
19
  } from "@bjornpagen/bumbledb"
21
- import { internalBlake3, lower } from "@bjornpagen/bumbledb"
20
+ import { internalDescriptor, lower } from "@bjornpagen/bumbledb"
22
21
  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"
22
+ import { regex } from "arkregex"
23
+ import { fromHex } from "#bytes.ts"
24
+ import type { Value } from "#value.ts"
26
25
 
27
26
  interface FieldInfo {
28
27
  readonly name: string
29
- readonly type: ValueTypeSpec
28
+ readonly type: SchemaSpec["relations"][number]["fields"][number]["valueType"]
30
29
  readonly fresh: boolean
31
30
  /** Set when the field's newtype names a closed relation's id class. */
32
31
  readonly closedRef: string | undefined
@@ -40,13 +39,13 @@ interface RelationInfo {
40
39
  /** Sealed order: a closed relation's synthetic u64 `id` at ordinal 0. */
41
40
  readonly fields: readonly FieldInfo[]
42
41
  /** Closed ground axioms in sealed order (id first), resolved values. */
43
- readonly rows: ReadonlyArray<readonly LogValue[]>
42
+ readonly rows: ReadonlyArray<readonly Value[]>
44
43
  }
45
44
 
46
45
  interface SideInfo {
47
46
  readonly relation: number
48
47
  readonly projection: readonly number[]
49
- readonly selection: ReadonlyArray<{ readonly field: number; readonly values: readonly LogValue[] }>
48
+ readonly selection: ReadonlyArray<{ readonly field: number; readonly values: readonly Value[] }>
50
49
  }
51
50
 
52
51
  type WeightInfo =
@@ -78,38 +77,125 @@ type StatementInfo =
78
77
  readonly source: SideInfo
79
78
  }
80
79
 
80
+ declare const braidBrand: unique symbol
81
+ type Braid = string & { readonly [braidBrand]: typeof braidBrand }
82
+
83
+ const BRAID_ID = regex("^c[0-9a-f]{8}$")
84
+ const ID_CLASS = regex("^(.*)\\.id$")
85
+
86
+ function braid(raw: string): Braid {
87
+ if (!BRAID_ID.test(raw)) {
88
+ throw errors.new(`not a braid id: ${raw}`)
89
+ }
90
+ return raw as Braid
91
+ }
92
+
93
+ function refuseShape(why: string): never {
94
+ throw errors.new(`theory: ${why}`)
95
+ }
96
+
97
+ /** `{name}.id` — the closed relation's generator class. */
98
+ function idClassOwner(newtype: string): string | undefined {
99
+ const match = ID_CLASS.exec(newtype)
100
+ const owner = match?.[1]
101
+ if (owner === undefined || owner.length === 0) {
102
+ return undefined
103
+ }
104
+ return owner
105
+ }
106
+
107
+ function indexRelations(relations: readonly RelationInfo[]): Map<number, RelationInfo> {
108
+ const byId = new Map<number, RelationInfo>()
109
+ for (const relation of relations) {
110
+ if (byId.has(relation.id)) {
111
+ refuseShape(`duplicate relation id ${relation.id}`)
112
+ }
113
+ byId.set(relation.id, relation)
114
+ }
115
+ return byId
116
+ }
117
+
118
+ function relationOfId(byId: ReadonlyMap<number, RelationInfo>, id: number): RelationInfo {
119
+ const relation = byId.get(id)
120
+ if (relation === undefined) {
121
+ refuseShape(`unknown relation id ${id}`)
122
+ }
123
+ return relation
124
+ }
125
+
126
+ function fieldNamed(relation: RelationInfo, name: string): { readonly ordinal: number; readonly field: FieldInfo } {
127
+ let ordinal = 0
128
+ for (const field of relation.fields) {
129
+ if (field.name === name) {
130
+ return { ordinal, field }
131
+ }
132
+ ordinal += 1
133
+ }
134
+ refuseShape(`relation ${relation.name} has no field ${name}`)
135
+ }
136
+
81
137
  interface SerialStatement {
82
138
  readonly statement: number
83
- readonly braid: string
139
+ readonly braid: Braid
84
140
  }
85
141
 
86
- interface LogDescriptor {
142
+ function serialAtOf(
143
+ byId: ReadonlyMap<number, RelationInfo>,
144
+ statements: readonly StatementInfo[],
145
+ braidOfRelation: ReadonlyMap<number, Braid>
146
+ ): SerialStatement[] {
147
+ const serialAt: SerialStatement[] = []
148
+ for (const statement of statements) {
149
+ if (statement.kind === "functionality" && statement.projection.length === 0) {
150
+ const relation = relationOfId(byId, statement.relation)
151
+ if (relation.fields.length > 0) {
152
+ const braid = braidOfRelation.get(statement.relation)
153
+ if (braid !== undefined) {
154
+ serialAt.push({ statement: statement.id, braid })
155
+ }
156
+ }
157
+ }
158
+ if (statement.kind === "capacity" && statement.target.projection.length === 0) {
159
+ const targetRelation = relationOfId(byId, statement.target.relation)
160
+ if (!targetRelation.closed) {
161
+ const braid = braidOfRelation.get(statement.target.relation)
162
+ if (braid === undefined) {
163
+ refuseShape(`capacity ${statement.id} target ${targetRelation.name} is not in the derived braid set`)
164
+ }
165
+ serialAt.push({ statement: statement.id, braid })
166
+ }
167
+ }
168
+ }
169
+ return serialAt
170
+ }
171
+
172
+ interface Descriptor {
87
173
  readonly relations: readonly RelationInfo[]
88
174
  readonly relationByName: ReadonlyMap<string, RelationInfo>
89
175
  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[]>
176
+ /** Ordinary relation id → braid id (`c{smallest:08x}`). */
177
+ readonly braidOfRelation: ReadonlyMap<number, Braid>
178
+ /** Braid id → member relation ids, ascending. */
179
+ readonly braidMembers: ReadonlyMap<Braid, readonly number[]>
94
180
  readonly serialAtStatements: readonly SerialStatement[]
95
181
  readonly fingerprint: string
96
182
  readonly fingerprintBytes: Uint8Array
97
183
  }
98
184
 
99
185
  /** The pure trio's input: the theory value, its lowered spec, or an already-parsed descriptor. */
100
- type LogTheory = AnySchema | SchemaSpec | LogDescriptor
186
+ type Theory = AnySchema | SchemaSpec | Descriptor
101
187
 
102
- function isDescriptor(theory: LogTheory): theory is LogDescriptor {
188
+ function isDescriptor(theory: Theory): theory is Descriptor {
103
189
  return "braidMembers" in theory
104
190
  }
105
191
 
106
- function isSpec(theory: LogTheory): theory is SchemaSpec {
192
+ function isSpec(theory: Theory): theory is SchemaSpec {
107
193
  return Array.isArray((theory as SchemaSpec).relations)
108
194
  }
109
195
 
110
- const cache = new WeakMap<object, LogDescriptor>()
196
+ const cache = new WeakMap<object, Descriptor>()
111
197
 
112
- function descriptorOf(theory: LogTheory): LogDescriptor {
198
+ function descriptorOf(theory: Theory): Descriptor {
113
199
  if (isDescriptor(theory)) {
114
200
  return theory
115
201
  }
@@ -118,17 +204,192 @@ function descriptorOf(theory: LogTheory): LogDescriptor {
118
204
  return hit
119
205
  }
120
206
  const spec = isSpec(theory) ? theory : lower(theory)
121
- const parsed = spec === theory ? parseSpec(spec) : (cache.get(spec) ?? parseSpec(spec))
207
+ const parsed = spec === theory ? fromSealed(spec) : (cache.get(spec) ?? fromSealed(spec))
122
208
  cache.set(theory, parsed)
123
209
  cache.set(spec, parsed)
124
210
  return parsed
125
211
  }
126
212
 
127
- function braidHex(relationId: number): string {
128
- return `c${relationId.toString(16).padStart(8, "0")}`
213
+ function braidHex(relationId: number): Braid {
214
+ return braid(`c${relationId.toString(16).padStart(8, "0")}`)
215
+ }
216
+
217
+ function asValue(raw: unknown): Value {
218
+ if (typeof raw === "boolean" || typeof raw === "bigint" || typeof raw === "string" || raw instanceof Uint8Array) {
219
+ return raw
220
+ }
221
+ if (typeof raw === "object" && raw !== null && "start" in raw && "end" in raw) {
222
+ const interval = raw as { start: unknown; end: unknown }
223
+ if (typeof interval.start === "bigint" && typeof interval.end === "bigint") {
224
+ return { start: interval.start, end: interval.end }
225
+ }
226
+ }
227
+ refuseShape("sealed value is not a raw fact value")
228
+ }
229
+
230
+ function sideOf(side: SealedSide): SideInfo {
231
+ return {
232
+ relation: side.relation,
233
+ projection: side.projection,
234
+ selection: side.selection.map(function valuesOf(binding) {
235
+ return { field: binding.field, values: binding.values.map(asValue) }
236
+ })
237
+ }
238
+ }
239
+
240
+ function statementOf(statement: SealedStatement): StatementInfo {
241
+ switch (statement.kind) {
242
+ case "functionality":
243
+ return statement
244
+ case "containment":
245
+ return {
246
+ id: statement.id,
247
+ kind: "containment",
248
+ source: sideOf(statement.source),
249
+ target: sideOf(statement.target)
250
+ }
251
+ case "capacity":
252
+ return {
253
+ id: statement.id,
254
+ kind: "capacity",
255
+ target: sideOf(statement.target),
256
+ weight: statement.weight,
257
+ lo: statement.lo,
258
+ hi: statement.hi,
259
+ source: sideOf(statement.source)
260
+ }
261
+ }
262
+ }
263
+
264
+ function fromSealed(spec: SchemaSpec): Descriptor {
265
+ const sealed: SealedDescriptor = internalDescriptor(spec)
266
+ const specByName = new Map<string, SchemaSpec["relations"][number]>()
267
+ for (const relation of spec.relations) {
268
+ if (specByName.has(relation.name)) {
269
+ refuseShape(`duplicate relation ${relation.name}`)
270
+ }
271
+ specByName.set(relation.name, relation)
272
+ }
273
+ const closedOwners = new Set(
274
+ spec.relations
275
+ .filter(function closedOf(relation) {
276
+ return relation.closed !== undefined
277
+ })
278
+ .map(function nameOf(relation) {
279
+ return relation.name
280
+ })
281
+ )
282
+
283
+ const relations: RelationInfo[] = sealed.relations.map(function relationOf(relation) {
284
+ const declared = specByName.get(relation.name)
285
+ if (declared === undefined) {
286
+ refuseShape(`sealed relation ${relation.name} is not in the spec`)
287
+ }
288
+ const closed = relation.extension !== undefined
289
+ const fields: FieldInfo[] = relation.fields.map(function fieldOf(field) {
290
+ if (field.name === "id" && closed) {
291
+ return { name: field.name, type: field.valueType, fresh: false, closedRef: relation.name }
292
+ }
293
+ const specField = declared.fields.find(function named(candidate) {
294
+ return candidate.name === field.name
295
+ })
296
+ let closedRef: string | undefined
297
+ const owner = specField?.newtype === undefined ? undefined : idClassOwner(specField.newtype)
298
+ if (owner !== undefined && closedOwners.has(owner)) {
299
+ closedRef = owner
300
+ }
301
+ return {
302
+ name: field.name,
303
+ type: field.valueType,
304
+ fresh: specField?.fresh === true,
305
+ closedRef
306
+ }
307
+ })
308
+ const extension = relation.extension ?? []
309
+ const named = new Map(
310
+ relation.fields.map(function indexOf(field) {
311
+ return [field.name, field]
312
+ })
313
+ )
314
+ const rows = extension.map(function rowOf(row) {
315
+ const cells = new Map(
316
+ row.values.map(function cellOf(cell) {
317
+ return [cell.name, asValue(cell.value)]
318
+ })
319
+ )
320
+ return relation.fields.map(function cellValue(field) {
321
+ const hit = cells.get(field.name)
322
+ if (hit !== undefined) {
323
+ return hit
324
+ }
325
+ if (field.name === "id" && named.has("id")) {
326
+ return row.id
327
+ }
328
+ return refuseShape(`closed relation ${relation.name}: sealed row ${row.handle} missing field ${field.name}`)
329
+ })
330
+ })
331
+ return {
332
+ id: relation.id,
333
+ name: relation.name,
334
+ closed,
335
+ handles: extension.map(function handleOf(row) {
336
+ return row.handle
337
+ }),
338
+ fields,
339
+ rows
340
+ }
341
+ })
342
+
343
+ const byName = new Map<string, RelationInfo>()
344
+ for (const relation of relations) {
345
+ if (byName.has(relation.name)) {
346
+ refuseShape(`duplicate relation ${relation.name}`)
347
+ }
348
+ byName.set(relation.name, relation)
349
+ }
350
+
351
+ const statements = sealed.statements.map(statementOf)
352
+ const byId = indexRelations(relations)
353
+ const { braidOfRelation, braidMembers } = deriveBraids(byId, statements)
354
+ const serialAtStatements = serialAtOf(byId, statements, braidOfRelation)
355
+
356
+ const fingerprint = sealed.fingerprint
357
+ return {
358
+ relations,
359
+ relationByName: byName,
360
+ statements,
361
+ braidOfRelation,
362
+ braidMembers,
363
+ serialAtStatements,
364
+ fingerprint,
365
+ fingerprintBytes: fromHex(fingerprint)
366
+ }
367
+ }
368
+
369
+ /**
370
+ * The same descriptor under a pinned fingerprint — for stores whose
371
+ * identity is carried (a manifest, a conformance sidecar) rather than
372
+ * recomputed.
373
+ */
374
+ function withFingerprint(descriptor: Descriptor, fingerprint: string): Descriptor {
375
+ return {
376
+ relations: descriptor.relations,
377
+ relationByName: descriptor.relationByName,
378
+ statements: descriptor.statements,
379
+ braidOfRelation: descriptor.braidOfRelation,
380
+ braidMembers: descriptor.braidMembers,
381
+ serialAtStatements: descriptor.serialAtStatements,
382
+ fingerprint,
383
+ fingerprintBytes: fromHex(fingerprint)
384
+ }
129
385
  }
130
386
 
131
- function logValueOf(value: ValueSpec): LogValue {
387
+ /**
388
+ * Assemble a descriptor from a SchemaSpec that is not a theory — the
389
+ * conformance corpus pins the codec and the braid map on shapes the
390
+ * engine seal refuses. Theories enter through descriptorOf.
391
+ */
392
+ function rawValue(value: ValueSpec): Value {
132
393
  switch (value.kind) {
133
394
  case "bool":
134
395
  return value.value
@@ -147,8 +408,39 @@ function logValueOf(value: ValueSpec): LogValue {
147
408
 
148
409
  interface SpecTables {
149
410
  readonly spec: SchemaSpec
150
- readonly relations: RelationInfo[]
151
411
  readonly byName: Map<string, RelationInfo>
412
+ readonly byId: Map<number, RelationInfo>
413
+ }
414
+
415
+ function zipClosedPayload(
416
+ relation: RelationInfo,
417
+ handle: string,
418
+ literals: readonly LiteralSpec[]
419
+ ): Array<readonly [FieldInfo, LiteralSpec]> {
420
+ const pairs: Array<readonly [FieldInfo, LiteralSpec]> = []
421
+ const pending = literals[Symbol.iterator]()
422
+ let seenId = false
423
+ for (const field of relation.fields) {
424
+ if (!seenId) {
425
+ if (field.name !== "id") {
426
+ refuseShape(`closed relation ${relation.name} has no sealed id field`)
427
+ }
428
+ seenId = true
429
+ continue
430
+ }
431
+ const next = pending.next()
432
+ if (next.done) {
433
+ refuseShape(`closed row ${handle} of ${relation.name}: fewer cells than payload fields`)
434
+ }
435
+ pairs.push([field, next.value])
436
+ }
437
+ if (!seenId) {
438
+ refuseShape(`closed relation ${relation.name} has no sealed id field`)
439
+ }
440
+ if (!pending.next().done) {
441
+ refuseShape(`closed row ${handle} of ${relation.name}: more cells than payload fields`)
442
+ }
443
+ return pairs
152
444
  }
153
445
 
154
446
  function fieldsOf(
@@ -166,8 +458,8 @@ function fieldsOf(
166
458
  }
167
459
  for (const field of relation.fields) {
168
460
  let closedRef: string | undefined
169
- if (field.newtype?.endsWith(".id")) {
170
- const owner = field.newtype.slice(0, -3)
461
+ const owner = field.newtype === undefined ? undefined : idClassOwner(field.newtype)
462
+ if (owner !== undefined) {
171
463
  const target = tables.get(owner)
172
464
  if (target?.closed === true) {
173
465
  closedRef = owner
@@ -178,27 +470,25 @@ function fieldsOf(
178
470
  return fields
179
471
  }
180
472
 
181
- function resolveLiteral(tables: SpecTables, relation: RelationInfo, field: FieldInfo, literal: LiteralSpec): LogValue {
473
+ function resolveLiteral(tables: SpecTables, relation: RelationInfo, field: FieldInfo, literal: LiteralSpec): Value {
182
474
  if (literal.kind === "value") {
183
- return logValueOf(literal.value)
475
+ return rawValue(literal.value)
184
476
  }
185
477
  if (field.closedRef === undefined) {
186
- throw errors.new(
187
- `handle literal ${literal.handle} on ${relation.name}.${field.name}, which references no closed roster`
188
- )
478
+ refuseShape(`handle literal ${literal.handle} on ${relation.name}.${field.name}, which references no closed roster`)
189
479
  }
190
480
  const target = tables.byName.get(field.closedRef)
191
481
  if (target === undefined) {
192
- throw errors.new(`handle literal ${literal.handle}: unknown closed relation ${field.closedRef}`)
482
+ refuseShape(`handle literal ${literal.handle}: unknown closed relation ${field.closedRef}`)
193
483
  }
194
484
  const id = target.handles.indexOf(literal.handle)
195
485
  if (id === -1) {
196
- throw errors.new(`handle literal ${literal.handle} is not in the ${target.name} roster`)
486
+ refuseShape(`handle literal ${literal.handle} is not in the ${target.name} roster`)
197
487
  }
198
488
  return BigInt(id)
199
489
  }
200
490
 
201
- function literalSetOf(tables: SpecTables, relation: RelationInfo, field: FieldInfo, set: LiteralSetSpec): LogValue[] {
491
+ function literalSetOf(tables: SpecTables, relation: RelationInfo, field: FieldInfo, set: LiteralSetSpec): Value[] {
202
492
  if (set.kind === "one") {
203
493
  return [resolveLiteral(tables, relation, field, set.literal)]
204
494
  }
@@ -208,29 +498,19 @@ function literalSetOf(tables: SpecTables, relation: RelationInfo, field: FieldIn
208
498
  }
209
499
 
210
500
  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
501
+ return fieldNamed(relation, name).ordinal
218
502
  }
219
503
 
220
- function sideOf(tables: SpecTables, side: SideSpec): SideInfo {
504
+ function specSideOf(tables: SpecTables, side: SideSpec): SideInfo {
221
505
  const relation = tables.byName.get(side.relation)
222
506
  if (relation === undefined) {
223
- throw errors.new(`statement cites unknown relation ${side.relation}`)
507
+ refuseShape(`statement cites unknown relation ${side.relation}`)
224
508
  }
225
509
  const projection = side.projection.map(function ordinalOf(name) {
226
510
  return fieldOrdinal(relation, name)
227
511
  })
228
512
  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
- }
513
+ const { ordinal, field } = fieldNamed(relation, binding[0])
234
514
  return { field: ordinal, values: literalSetOf(tables, relation, field, binding[1]) }
235
515
  })
236
516
  return { relation: relation.id, projection, selection }
@@ -238,7 +518,7 @@ function sideOf(tables: SpecTables, side: SideSpec): SideInfo {
238
518
 
239
519
  function boundValue(context: string, bound: { readonly kind: string }): bigint {
240
520
  if (bound.kind !== "lit") {
241
- throw errors.new(`${context}: dependent floors are refused by the schema grammar`)
521
+ refuseShape(`${context}: dependent floors are refused by the schema grammar`)
242
522
  }
243
523
  return (bound as { readonly kind: "lit"; readonly value: bigint }).value
244
524
  }
@@ -248,12 +528,12 @@ function capacityOf(
248
528
  id: number,
249
529
  statement: Extract<StatementSpec, { kind: "capacity" }>
250
530
  ): StatementInfo {
251
- const target = sideOf(tables, statement.target)
252
- const source = sideOf(tables, statement.source)
531
+ const target = specSideOf(tables, statement.target)
532
+ const source = specSideOf(tables, statement.source)
253
533
  const sourceRelation = tables.byName.get(statement.source.relation)
254
534
  const targetRelation = tables.byName.get(statement.target.relation)
255
535
  if (sourceRelation === undefined || targetRelation === undefined) {
256
- throw errors.new("capacity statement cites unknown relations")
536
+ refuseShape("capacity statement cites unknown relations")
257
537
  }
258
538
  let weight: WeightInfo
259
539
  switch (statement.weight.kind) {
@@ -306,56 +586,62 @@ function capacityOf(
306
586
  return { id, kind: "capacity", target, weight, lo, hi, source }
307
587
  }
308
588
 
309
- /** The `exact {n}` window: lo = hi = n; a non-literal n is grammar-refused upstream. */
310
-
311
- function parseSpec(spec: SchemaSpec): LogDescriptor {
589
+ function assembleFromSpec(spec: SchemaSpec): Descriptor {
312
590
  const prepass = new Map<string, { closed: boolean; handles: readonly string[] }>()
313
591
  for (const relation of spec.relations) {
592
+ if (prepass.has(relation.name)) {
593
+ refuseShape(`duplicate relation ${relation.name}`)
594
+ }
314
595
  prepass.set(relation.name, {
315
596
  closed: relation.closed !== undefined,
316
597
  handles: relation.closed === undefined ? [] : relation.closed.rows.map((row) => row.handle)
317
598
  })
318
599
  }
319
600
 
320
- const relations: RelationInfo[] = []
321
601
  const byName = new Map<string, RelationInfo>()
322
- spec.relations.forEach(function buildRelation(relation, id) {
323
- const fields = fieldsOf(prepass, relation)
602
+ const byId = new Map<number, RelationInfo>()
603
+ let nextId = 0
604
+ for (const relation of spec.relations) {
605
+ const table = prepass.get(relation.name)
606
+ if (table === undefined) {
607
+ refuseShape(`relation ${relation.name} missing from the closedness table`)
608
+ }
324
609
  const info: RelationInfo = {
325
- id,
610
+ id: nextId,
326
611
  name: relation.name,
327
- closed: relation.closed !== undefined,
328
- handles: prepass.get(relation.name)?.handles ?? [],
329
- fields,
612
+ closed: table.closed,
613
+ handles: table.handles,
614
+ fields: fieldsOf(prepass, relation),
330
615
  rows: []
331
616
  }
332
- relations.push(info)
617
+ nextId += 1
333
618
  byName.set(relation.name, info)
334
- })
335
- const tables: SpecTables = { spec, relations, byName }
619
+ byId.set(info.id, info)
620
+ }
336
621
 
337
- spec.relations.forEach(function resolveRows(relation, id) {
622
+ const tables: SpecTables = { spec, byName, byId }
623
+
624
+ for (const relation of spec.relations) {
338
625
  if (relation.closed === undefined) {
339
- return
626
+ continue
340
627
  }
341
- const info = relations[id]
628
+ const info = byName.get(relation.name)
342
629
  if (info === undefined) {
343
- throw errors.new(`relation ordinal ${id} missing`)
630
+ refuseShape(`closed relation ${relation.name} missing`)
344
631
  }
345
632
  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
- }
633
+ const values: Value[] = [BigInt(rowId)]
634
+ for (const [field, literal] of zipClosedPayload(info, row.handle, row.values)) {
352
635
  values.push(resolveLiteral(tables, info, field, literal))
353
- })
636
+ }
354
637
  return values
355
638
  })
356
- relations[id] = { ...info, rows }
357
- byName.set(relation.name, relations[id])
358
- })
639
+ const updated = { ...info, rows }
640
+ byName.set(relation.name, updated)
641
+ byId.set(info.id, updated)
642
+ }
643
+
644
+ const relations = [...byId.values()]
359
645
 
360
646
  const statements: StatementInfo[] = []
361
647
  relations.forEach(function freshKeys(relation) {
@@ -375,7 +661,7 @@ function parseSpec(spec: SchemaSpec): LogDescriptor {
375
661
  case "fd": {
376
662
  const relation = byName.get(statement.relation)
377
663
  if (relation === undefined) {
378
- throw errors.new(`key statement cites unknown relation ${statement.relation}`)
664
+ refuseShape(`key statement cites unknown relation ${statement.relation}`)
379
665
  }
380
666
  statements.push({
381
667
  id: statements.length,
@@ -388,8 +674,8 @@ function parseSpec(spec: SchemaSpec): LogDescriptor {
388
674
  break
389
675
  }
390
676
  case "containment": {
391
- const source = sideOf(tables, statement.source)
392
- const target = sideOf(tables, statement.target)
677
+ const source = specSideOf(tables, statement.source)
678
+ const target = specSideOf(tables, statement.target)
393
679
  statements.push({ id: statements.length, kind: "containment", source, target })
394
680
  if (statement.bidirectional) {
395
681
  statements.push({ id: statements.length, kind: "containment", source: target, target: source })
@@ -403,78 +689,28 @@ function parseSpec(spec: SchemaSpec): LogDescriptor {
403
689
  }
404
690
  }
405
691
 
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
- }
692
+ const { braidOfRelation, braidMembers } = deriveBraids(byId, statements)
693
+ const serialAtStatements = serialAtOf(byId, statements, braidOfRelation)
435
694
 
436
- const descriptor: LogDescriptor = {
695
+ const fingerprint = "00".repeat(32)
696
+ return {
437
697
  relations,
438
698
  relationByName: byName,
439
699
  statements,
440
700
  braidOfRelation,
441
701
  braidMembers,
442
702
  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
703
  fingerprint,
468
704
  fingerprintBytes: fromHex(fingerprint)
469
705
  }
470
706
  }
471
707
 
472
708
  function deriveBraids(
473
- relations: readonly RelationInfo[],
709
+ byId: ReadonlyMap<number, RelationInfo>,
474
710
  statements: readonly StatementInfo[]
475
- ): { braidOfRelation: Map<number, string>; braidMembers: Map<string, readonly number[]> } {
711
+ ): { braidOfRelation: Map<number, Braid>; braidMembers: Map<Braid, readonly number[]> } {
476
712
  const parent = new Map<number, number>()
477
- for (const relation of relations) {
713
+ for (const relation of byId.values()) {
478
714
  if (!relation.closed) {
479
715
  parent.set(relation.id, relation.id)
480
716
  }
@@ -500,9 +736,9 @@ function deriveBraids(
500
736
  if (statement.kind === "functionality") {
501
737
  continue
502
738
  }
503
- const source = relations[statement.source.relation]
504
- const target = relations[statement.target.relation]
505
- if (source === undefined || target === undefined || source.closed || target.closed) {
739
+ const source = relationOfId(byId, statement.source.relation)
740
+ const target = relationOfId(byId, statement.target.relation)
741
+ if (source.closed || target.closed) {
506
742
  continue
507
743
  }
508
744
  union(source.id, target.id)
@@ -517,8 +753,8 @@ function deriveBraids(
517
753
  list.push(id)
518
754
  }
519
755
  }
520
- const braidOfRelation = new Map<number, string>()
521
- const braidMembers = new Map<string, readonly number[]>()
756
+ const braidOfRelation = new Map<number, Braid>()
757
+ const braidMembers = new Map<Braid, readonly number[]>()
522
758
  for (const [root, ids] of [...members.entries()].sort(function byRoot(a, b) {
523
759
  return a[0] - b[0]
524
760
  })) {
@@ -534,208 +770,16 @@ function deriveBraids(
534
770
  return { braidOfRelation, braidMembers }
535
771
  }
536
772
 
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
773
  export type {
774
+ Braid,
775
+ Descriptor,
731
776
  FieldInfo,
732
777
  HiInfo,
733
- LogDescriptor,
734
- LogTheory,
735
778
  RelationInfo,
736
779
  SerialStatement,
737
780
  SideInfo,
738
781
  StatementInfo,
782
+ Theory,
739
783
  WeightInfo
740
784
  }
741
- export { braidHex, descriptorOf, withFingerprint }
785
+ export { assembleFromSpec, braid, braidHex, descriptorOf, withFingerprint }